OLD | NEW |
| (Empty) |
1 // Copyright 2016 The LUCI Authors. All rights reserved. | |
2 // Use of this source code is governed under the Apache License, Version 2.0 | |
3 // that can be found in the LICENSE file. | |
4 | |
5 package buildbot | |
6 | |
7 import ( | |
8 "bytes" | |
9 "compress/gzip" | |
10 "encoding/json" | |
11 "fmt" | |
12 "io/ioutil" | |
13 "strconv" | |
14 | |
15 "github.com/luci/gae/service/datastore" | |
16 "github.com/luci/luci-go/milo/api/resp" | |
17 "github.com/luci/luci-go/milo/appengine/common/model" | |
18 ) | |
19 | |
20 // This file contains all of the structs that define buildbot json endpoints. | |
21 // This is primarily used for unmarshalling buildbot master and build json. | |
22 // json.UnmarshalJSON can directly unmarshal buildbot jsons into these structs. | |
23 // Many of the structs were initially built using https://mholt.github.io/json-t
o-go/ | |
24 | |
25 // buildbotStep represents a single step in a buildbot build. | |
26 type buildbotStep struct { | |
27 // We actually don't care about ETA. This is represented as a string if | |
28 // it's fetched from a build json, but a float if it's dug out of the | |
29 // slave portion of a master json. We'll just set it to interface and | |
30 // ignore it. | |
31 Eta interface{} `json:"eta"` | |
32 Expectations [][]interface{} `json:"expectations"` | |
33 Hidden bool `json:"hidden"` | |
34 IsFinished bool `json:"isFinished"` | |
35 IsStarted bool `json:"isStarted"` | |
36 Logs [][]string `json:"logs"` | |
37 Name string `json:"name"` | |
38 Results []interface{} `json:"results"` | |
39 Statistics struct { | |
40 } `json:"statistics"` | |
41 StepNumber int `json:"step_number"` | |
42 Text []string `json:"text"` | |
43 Times []*float64 `json:"times"` | |
44 Urls map[string]string `json:"urls"` | |
45 | |
46 // Log link aliases. The key is a log name that is being aliases. It sh
ould, | |
47 // generally, exist within the Logs. The value is the set of aliases att
ached | |
48 // to that key. | |
49 Aliases map[string][]*buildbotLinkAlias `json:"aliases"` | |
50 } | |
51 | |
52 // buildbotSourceStamp is a list of changes (commits) tagged with where the chan
ges | |
53 // came from, ie. the project/repository. Also includes a "main" revision." | |
54 type buildbotSourceStamp struct { | |
55 Branch *string `json:"branch"` | |
56 Changes []buildbotChange `json:"changes"` | |
57 Haspatch bool `json:"hasPatch"` | |
58 Project string `json:"project"` | |
59 Repository string `json:"repository"` | |
60 Revision string `json:"revision"` | |
61 } | |
62 | |
63 type buildbotLinkAlias struct { | |
64 URL string `json:"url"` | |
65 Text string `json:"text"` | |
66 } | |
67 | |
68 func (a *buildbotLinkAlias) toLink() *resp.Link { | |
69 return resp.NewLink(a.Text, a.URL) | |
70 } | |
71 | |
72 type buildbotProperty struct { | |
73 Name string | |
74 Value interface{} | |
75 Source string | |
76 } | |
77 | |
78 func (p *buildbotProperty) MarshalJSON() ([]byte, error) { | |
79 return json.Marshal([]interface{}{p.Name, p.Value, p.Source}) | |
80 } | |
81 | |
82 func (p *buildbotProperty) UnmarshalJSON(d []byte) error { | |
83 // The raw BuildBot representation is a slice of interfaces. | |
84 var raw []interface{} | |
85 if err := json.Unmarshal(d, &raw); err != nil { | |
86 return err | |
87 } | |
88 | |
89 switch len(raw) { | |
90 case 3: | |
91 if s, ok := raw[2].(string); ok { | |
92 p.Source = s | |
93 } | |
94 fallthrough | |
95 | |
96 case 2: | |
97 p.Value = raw[1] | |
98 fallthrough | |
99 | |
100 case 1: | |
101 if s, ok := raw[0].(string); ok { | |
102 p.Name = s | |
103 } | |
104 } | |
105 return nil | |
106 } | |
107 | |
108 // buildbotBuild is a single build json on buildbot. | |
109 type buildbotBuild struct { | |
110 Master string `gae:"$master"` | |
111 Blame []string `json:"blame" gae:"-"` | |
112 Buildername string `json:"builderName"` | |
113 // This needs to be reflected. This can be either a String or a buildbo
tStep. | |
114 Currentstep interface{} `json:"currentStep" gae:"-"` | |
115 // We don't care about this one. | |
116 Eta interface{} `json:"eta" gae:"-"` | |
117 Logs [][]string `json:"logs" gae:"-"` | |
118 Number int `json:"number"` | |
119 // This is a slice of tri-tuples of [property name, value, source]. | |
120 // property name is always a string | |
121 // value can be a string or float | |
122 // source is optional, but is always a string if present | |
123 Properties []*buildbotProperty `json:"properties" gae:"-"` | |
124 Reason string `json:"reason"` | |
125 Results *int `json:"results" gae:"-"` | |
126 Slave string `json:"slave"` | |
127 Sourcestamp *buildbotSourceStamp `json:"sourceStamp" gae:"-"` | |
128 Steps []buildbotStep `json:"steps" gae:"-"` | |
129 Text []string `json:"text" gae:"-"` | |
130 Times []*float64 `json:"times" gae:"-"` | |
131 // This one is injected by Milo. Does not exist in a normal json query. | |
132 TimeStamp *int `json:"timeStamp" gae:"-"` | |
133 // This one is marked by Milo, denotes whether or not the build is inter
nal. | |
134 Internal bool `json:"internal" gae:"-"` | |
135 // This one is computed by Milo for indexing purposes. It does so by | |
136 // checking to see if times[1] is null or not. | |
137 Finished bool `json:"finished"` | |
138 // OS is a string representation of the OS the build ran on. This is | |
139 // derived best-effort from the slave information in the master JSON. | |
140 // This information is injected into the buildbot builds via puppet, and | |
141 // comes as Family + Version. Family is (windows, Darwin, Debian), whil
e | |
142 // Version is the version of the OS, such as (XP, 7, 10) for windows. | |
143 OSFamily string `json:"osFamily"` | |
144 OSVersion string `json:"osVersion"` | |
145 } | |
146 | |
147 func (b *buildbotBuild) toStatus() model.Status { | |
148 var result model.Status | |
149 if b.Currentstep != nil { | |
150 result = model.Running | |
151 } else { | |
152 result = result2Status(b.Results) | |
153 } | |
154 return result | |
155 } | |
156 | |
157 var _ datastore.PropertyLoadSaver = (*buildbotBuild)(nil) | |
158 var _ datastore.MetaGetterSetter = (*buildbotBuild)(nil) | |
159 | |
160 // getID is a helper function that returns the datastore key for a given | |
161 // build. | |
162 func (b *buildbotBuild) getID() string { | |
163 s := []string{b.Master, b.Buildername, strconv.Itoa(b.Number)} | |
164 id, err := json.Marshal(s) | |
165 if err != nil { | |
166 panic(err) // This really shouldn't fail. | |
167 } | |
168 return string(id) | |
169 } | |
170 | |
171 // setKeys is the inverse of getID(). | |
172 func (b *buildbotBuild) setKeys(id string) error { | |
173 s := []string{} | |
174 err := json.Unmarshal([]byte(id), &s) | |
175 if err != nil { | |
176 return err | |
177 } | |
178 if len(s) != 3 { | |
179 return fmt.Errorf("%s does not have 3 items", id) | |
180 } | |
181 b.Master = s[0] | |
182 b.Buildername = s[1] | |
183 b.Number, err = strconv.Atoi(s[2]) | |
184 return err // or nil. | |
185 } | |
186 | |
187 // GetMeta is overridden so that a query for "id" calls getID() instead of | |
188 // the superclass method. | |
189 func (b *buildbotBuild) GetMeta(key string) (interface{}, bool) { | |
190 if key == "id" { | |
191 if b.Master == "" || b.Buildername == "" { | |
192 panic(fmt.Errorf("No Master or Builder found")) | |
193 } | |
194 return b.getID(), true | |
195 } | |
196 return datastore.GetPLS(b).GetMeta(key) | |
197 } | |
198 | |
199 // GetAllMeta is overridden for the same reason GetMeta() is. | |
200 func (b *buildbotBuild) GetAllMeta() datastore.PropertyMap { | |
201 p := datastore.GetPLS(b).GetAllMeta() | |
202 p.SetMeta("id", b.getID()) | |
203 return p | |
204 } | |
205 | |
206 // SetMeta is the inverse of GetMeta(). | |
207 func (b *buildbotBuild) SetMeta(key string, val interface{}) bool { | |
208 if key == "id" { | |
209 err := b.setKeys(val.(string)) | |
210 if err != nil { | |
211 panic(err) | |
212 } | |
213 } | |
214 return datastore.GetPLS(b).SetMeta(key, val) | |
215 } | |
216 | |
217 // Load translates a propertymap into the struct and loads the data into | |
218 // the struct. | |
219 func (b *buildbotBuild) Load(p datastore.PropertyMap) error { | |
220 if _, ok := p["data"]; !ok { | |
221 // This is probably from a keys-only query. No need to load the
rest. | |
222 return datastore.GetPLS(b).Load(p) | |
223 } | |
224 gz, err := p.Slice("data")[0].Project(datastore.PTBytes) | |
225 if err != nil { | |
226 return err | |
227 } | |
228 reader, err := gzip.NewReader(bytes.NewReader(gz.([]byte))) | |
229 if err != nil { | |
230 return err | |
231 } | |
232 bs, err := ioutil.ReadAll(reader) | |
233 if err != nil { | |
234 return err | |
235 } | |
236 return json.Unmarshal(bs, b) | |
237 } | |
238 | |
239 func (b *buildbotBuild) getPropertyValue(name string) interface{} { | |
240 for _, prop := range b.Properties { | |
241 if prop.Name == name { | |
242 return prop.Value | |
243 } | |
244 } | |
245 return "" | |
246 } | |
247 | |
248 type errTooBig struct { | |
249 error | |
250 } | |
251 | |
252 // Save returns a property map of the struct to save in the datastore. It | |
253 // contains two fields, the ID which is the key, and a data field which is a | |
254 // serialized and gzipped representation of the entire struct. | |
255 func (b *buildbotBuild) Save(withMeta bool) (datastore.PropertyMap, error) { | |
256 bs, err := json.Marshal(b) | |
257 if err != nil { | |
258 return nil, err | |
259 } | |
260 gzbs := bytes.Buffer{} | |
261 gsw := gzip.NewWriter(&gzbs) | |
262 _, err = gsw.Write(bs) | |
263 if err != nil { | |
264 return nil, err | |
265 } | |
266 err = gsw.Close() | |
267 if err != nil { | |
268 return nil, err | |
269 } | |
270 blob := gzbs.Bytes() | |
271 // Datastore has a max size of 1MB. If the blob is over 9.5MB, it proba
bly | |
272 // won't fit after accounting for overhead. | |
273 if len(blob) > 950000 { | |
274 return nil, errTooBig{ | |
275 fmt.Errorf("buildbotBuild: Build too big to store (%d by
tes)", len(blob))} | |
276 } | |
277 p := datastore.PropertyMap{ | |
278 "data": datastore.MkPropertyNI(blob), | |
279 } | |
280 if withMeta { | |
281 p["id"] = datastore.MkPropertyNI(b.getID()) | |
282 p["master"] = datastore.MkProperty(b.Master) | |
283 p["builder"] = datastore.MkProperty(b.Buildername) | |
284 p["number"] = datastore.MkProperty(b.Number) | |
285 p["finished"] = datastore.MkProperty(b.Finished) | |
286 } | |
287 return p, nil | |
288 } | |
289 | |
290 type buildbotPending struct { | |
291 Source buildbotSourceStamp `json:"source"` | |
292 Reason string `json:"reason"` | |
293 SubmittedAt int `json:"submittedAt"` | |
294 BuilderName string `json:"builderName"` | |
295 } | |
296 | |
297 // buildbotBuilder is a builder struct from the master json, _not_ the builder j
son. | |
298 type buildbotBuilder struct { | |
299 Basedir string `json:"basedir"` | |
300 CachedBuilds []int `json:"cachedBuilds"` | |
301 PendingBuilds int `json:"pendingBuilds"` | |
302 // This one is specific to the pubsub interface. This is limited to 75, | |
303 // so it could differ from PendingBuilds | |
304 PendingBuildStates []*buildbotPending `json:"pendingBuildStates"` | |
305 Category string `json:"category"` | |
306 CurrentBuilds []int `json:"currentBuilds"` | |
307 Slaves []string `json:"slaves"` | |
308 State string `json:"state"` | |
309 } | |
310 | |
311 // buildbotChangeSource is a changesource (ie polling source) usually tied to a
master's scheduler. | |
312 type buildbotChangeSource struct { | |
313 Description string `json:"description"` | |
314 } | |
315 | |
316 // buildbotChange describes a commit in a repository as part of a changesource o
f blamelist. | |
317 type buildbotChange struct { | |
318 At string `json:"at"` | |
319 Branch *string `json:"branch"` | |
320 Category string `json:"category"` | |
321 Comments string `json:"comments"` | |
322 // This could be a list of strings or list of struct { Name string } . | |
323 Files []interface{} `json:"files"` | |
324 Number int `json:"number"` | |
325 Project string `json:"project"` | |
326 Properties [][]interface{} `json:"properties"` | |
327 Repository string `json:"repository"` | |
328 Rev string `json:"rev"` | |
329 Revision string `json:"revision"` | |
330 Revlink string `json:"revlink"` | |
331 When int `json:"when"` | |
332 Who string `json:"who"` | |
333 } | |
334 | |
335 func (bc *buildbotChange) GetFiles() []string { | |
336 files := make([]string, 0, len(bc.Files)) | |
337 for _, f := range bc.Files { | |
338 // Buildbot stores files both as a string, or as a dict with a s
ingle entry | |
339 // named "name". It doesn't matter to us what the type is, but
we need | |
340 // to reflect on the type anyways. | |
341 switch fn := f.(type) { | |
342 case string: | |
343 files = append(files, fn) | |
344 case map[string]interface{}: | |
345 if name, ok := fn["name"]; ok { | |
346 files = append(files, fmt.Sprintf("%s", name)) | |
347 } | |
348 } | |
349 } | |
350 return files | |
351 } | |
352 | |
353 // buildbotSlave describes a slave on a master from a master json, and also incl
udes the | |
354 // full builds of any currently running builds. | |
355 type buildbotSlave struct { | |
356 // RecentBuilds is a map of builder name to a list of recent finished bu
ild | |
357 // numbers on that builder. | |
358 RecentBuilds map[string][]int `json:"builders"` | |
359 Connected bool `json:"connected"` | |
360 Host string `json:"host"` | |
361 Name string `json:"name"` | |
362 Runningbuilds []*buildbotBuild `json:"runningBuilds"` | |
363 Version string `json:"version"` | |
364 // This is like runningbuilds, but instead of storing the full build, | |
365 // just reference the build by builder: build num. | |
366 RunningbuildsMap map[string][]int `json:"runningBuildsMap"` | |
367 } | |
368 | |
369 type buildbotProject struct { | |
370 BuildbotURL string `json:"buildbotURL"` | |
371 Title string `json:"title"` | |
372 Titleurl string `json:"titleURL"` | |
373 } | |
374 | |
375 // buildbotMaster This is json definition for https://build.chromium.org/p/<mast
er>/json | |
376 // endpoints. | |
377 type buildbotMaster struct { | |
378 AcceptingBuilds struct { | |
379 AcceptingBuilds bool `json:"accepting_builds"` | |
380 } `json:"accepting_builds"` | |
381 | |
382 Builders map[string]*buildbotBuilder `json:"builders"` | |
383 | |
384 Buildstate struct { | |
385 AcceptingBuilds bool `json:"accepting_builds"` | |
386 Builders []struct { | |
387 Basedir string `json:"basedir"` | |
388 Buildername string `json:"builderName"` | |
389 Cachedbuilds []int `json:"cachedBuilds"` | |
390 Category string `json:"category"` | |
391 Currentbuilds []int `json:"currentBuilds"` | |
392 Slaves []string `json:"slaves"` | |
393 State string `json:"state"` | |
394 } `json:"builders"` | |
395 Project struct { | |
396 BuildbotURL string `json:"buildbotURL"` | |
397 Title string `json:"title"` | |
398 Titleurl string `json:"titleURL"` | |
399 } `json:"project"` | |
400 Timestamp float64 `json:"timestamp"` | |
401 } `json:"buildstate"` | |
402 | |
403 ChangeSources map[string]buildbotChangeSource `json:"change_sources"` | |
404 | |
405 Changes map[string]buildbotChange `json:"changes"` | |
406 | |
407 Clock struct { | |
408 Current struct { | |
409 Local string `json:"local"` | |
410 Utc string `json:"utc"` | |
411 UtcTs float64 `json:"utc_ts"` | |
412 } `json:"current"` | |
413 ServerStarted struct { | |
414 Local string `json:"local"` | |
415 Utc string `json:"utc"` | |
416 UtcTs float64 `json:"utc_ts"` | |
417 } `json:"server_started"` | |
418 ServerUptime float64 `json:"server_uptime"` | |
419 } `json:"clock"` | |
420 | |
421 Project buildbotProject `json:"project"` | |
422 | |
423 Slaves map[string]*buildbotSlave `json:"slaves"` | |
424 | |
425 Varz struct { | |
426 AcceptingBuilds bool `json:"accepting_builds"` | |
427 Builders map[string]struct { | |
428 ConnectedSlaves int `json:"connected_slaves"` | |
429 CurrentBuilds int `json:"current_builds"` | |
430 PendingBuilds int `json:"pending_builds"` | |
431 State string `json:"state"` | |
432 TotalSlaves int `json:"total_slaves"` | |
433 } `json:"builders"` | |
434 ServerUptime float64 `json:"server_uptime"` | |
435 } `json:"varz"` | |
436 | |
437 // This is injected by the pubsub publisher on the buildbot side. | |
438 Name string `json:"name"` | |
439 } | |
OLD | NEW |