blob: c9e257938f9fa5f351a705124ee992d6d97eb176 [file]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buildbot
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"strconv"
"github.com/luci/gae/service/datastore"
"github.com/luci/luci-go/milo/api/resp"
"github.com/luci/luci-go/milo/common/model"
)
// This file contains all of the structs that define buildbot json endpoints.
// This is primarily used for unmarshalling buildbot master and build json.
// json.UnmarshalJSON can directly unmarshal buildbot jsons into these structs.
// Many of the structs were initially built using https://mholt.github.io/json-to-go/
// buildbotStep represents a single step in a buildbot build.
type buildbotStep struct {
// We actually don't care about ETA. This is represented as a string if
// it's fetched from a build json, but a float if it's dug out of the
// slave portion of a master json. We'll just set it to interface and
// ignore it.
Eta interface{} `json:"eta"`
Expectations [][]interface{} `json:"expectations"`
Hidden bool `json:"hidden"`
IsFinished bool `json:"isFinished"`
IsStarted bool `json:"isStarted"`
Logs [][]string `json:"logs"`
Name string `json:"name"`
Results []interface{} `json:"results"`
Statistics struct {
} `json:"statistics"`
StepNumber int `json:"step_number"`
Text []string `json:"text"`
Times []*float64 `json:"times"`
Urls map[string]string `json:"urls"`
// Log link aliases. The key is a log name that is being aliases. It should,
// generally, exist within the Logs. The value is the set of aliases attached
// to that key.
Aliases map[string][]*buildbotLinkAlias `json:"aliases"`
}
// buildbotSourceStamp is a list of changes (commits) tagged with where the changes
// came from, ie. the project/repository. Also includes a "main" revision."
type buildbotSourceStamp struct {
Branch *string `json:"branch"`
Changes []buildbotChange `json:"changes"`
Haspatch bool `json:"hasPatch"`
Project string `json:"project"`
Repository string `json:"repository"`
Revision string `json:"revision"`
}
type buildbotLinkAlias struct {
URL string `json:"url"`
Text string `json:"text"`
}
func (a *buildbotLinkAlias) toLink() *resp.Link {
return resp.NewLink(a.Text, a.URL)
}
type buildbotProperty struct {
Name string
Value interface{}
Source string
}
func (p *buildbotProperty) MarshalJSON() ([]byte, error) {
return json.Marshal([]interface{}{p.Name, p.Value, p.Source})
}
func (p *buildbotProperty) UnmarshalJSON(d []byte) error {
// The raw BuildBot representation is a slice of interfaces.
var raw []interface{}
if err := json.Unmarshal(d, &raw); err != nil {
return err
}
switch len(raw) {
case 3:
if s, ok := raw[2].(string); ok {
p.Source = s
}
fallthrough
case 2:
p.Value = raw[1]
fallthrough
case 1:
if s, ok := raw[0].(string); ok {
p.Name = s
}
}
return nil
}
// buildbotBuild is a single build json on buildbot.
type buildbotBuild struct {
Master string `gae:"$master"`
Blame []string `json:"blame" gae:"-"`
Buildername string `json:"builderName"`
// This needs to be reflected. This can be either a String or a buildbotStep.
Currentstep interface{} `json:"currentStep" gae:"-"`
// We don't care about this one.
Eta interface{} `json:"eta" gae:"-"`
Logs [][]string `json:"logs" gae:"-"`
Number int `json:"number"`
// This is a slice of tri-tuples of [property name, value, source].
// property name is always a string
// value can be a string or float
// source is optional, but is always a string if present
Properties []*buildbotProperty `json:"properties" gae:"-"`
Reason string `json:"reason"`
Results *int `json:"results" gae:"-"`
Slave string `json:"slave"`
Sourcestamp *buildbotSourceStamp `json:"sourceStamp" gae:"-"`
Steps []buildbotStep `json:"steps" gae:"-"`
Text []string `json:"text" gae:"-"`
Times []*float64 `json:"times" gae:"-"`
// This one is injected by Milo. Does not exist in a normal json query.
TimeStamp *int `json:"timeStamp" gae:"-"`
// This one is marked by Milo, denotes whether or not the build is internal.
Internal bool `json:"internal" gae:"-"`
// This one is computed by Milo for indexing purposes. It does so by
// checking to see if times[1] is null or not.
Finished bool `json:"finished"`
// OS is a string representation of the OS the build ran on. This is
// derived best-effort from the slave information in the master JSON.
// This information is injected into the buildbot builds via puppet, and
// comes as Family + Version. Family is (windows, Darwin, Debian), while
// Version is the version of the OS, such as (XP, 7, 10) for windows.
OSFamily string `json:"osFamily"`
OSVersion string `json:"osVersion"`
}
func (b *buildbotBuild) toStatus() model.Status {
var result model.Status
if b.Currentstep != nil {
result = model.Running
} else {
result = result2Status(b.Results)
}
return result
}
var _ datastore.PropertyLoadSaver = (*buildbotBuild)(nil)
var _ datastore.MetaGetterSetter = (*buildbotBuild)(nil)
// getID is a helper function that returns the datastore key for a given
// build.
func (b *buildbotBuild) getID() string {
s := []string{b.Master, b.Buildername, strconv.Itoa(b.Number)}
id, err := json.Marshal(s)
if err != nil {
panic(err) // This really shouldn't fail.
}
return string(id)
}
// setKeys is the inverse of getID().
func (b *buildbotBuild) setKeys(id string) error {
s := []string{}
err := json.Unmarshal([]byte(id), &s)
if err != nil {
return err
}
if len(s) != 3 {
return fmt.Errorf("%s does not have 3 items", id)
}
b.Master = s[0]
b.Buildername = s[1]
b.Number, err = strconv.Atoi(s[2])
return err // or nil.
}
// GetMeta is overridden so that a query for "id" calls getID() instead of
// the superclass method.
func (b *buildbotBuild) GetMeta(key string) (interface{}, bool) {
if key == "id" {
if b.Master == "" || b.Buildername == "" {
panic(fmt.Errorf("No Master or Builder found"))
}
return b.getID(), true
}
return datastore.GetPLS(b).GetMeta(key)
}
// GetAllMeta is overridden for the same reason GetMeta() is.
func (b *buildbotBuild) GetAllMeta() datastore.PropertyMap {
p := datastore.GetPLS(b).GetAllMeta()
p.SetMeta("id", b.getID())
return p
}
// SetMeta is the inverse of GetMeta().
func (b *buildbotBuild) SetMeta(key string, val interface{}) bool {
if key == "id" {
err := b.setKeys(val.(string))
if err != nil {
panic(err)
}
}
return datastore.GetPLS(b).SetMeta(key, val)
}
// Load translates a propertymap into the struct and loads the data into
// the struct.
func (b *buildbotBuild) Load(p datastore.PropertyMap) error {
if _, ok := p["data"]; !ok {
// This is probably from a keys-only query. No need to load the rest.
return datastore.GetPLS(b).Load(p)
}
gz, err := p.Slice("data")[0].Project(datastore.PTBytes)
if err != nil {
return err
}
reader, err := gzip.NewReader(bytes.NewReader(gz.([]byte)))
if err != nil {
return err
}
bs, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
return json.Unmarshal(bs, b)
}
func (b *buildbotBuild) getPropertyValue(name string) interface{} {
for _, prop := range b.Properties {
if prop.Name == name {
return prop.Value
}
}
return ""
}
type errTooBig struct {
error
}
// Save returns a property map of the struct to save in the datastore. It
// contains two fields, the ID which is the key, and a data field which is a
// serialized and gzipped representation of the entire struct.
func (b *buildbotBuild) Save(withMeta bool) (datastore.PropertyMap, error) {
bs, err := json.Marshal(b)
if err != nil {
return nil, err
}
gzbs := bytes.Buffer{}
gsw := gzip.NewWriter(&gzbs)
_, err = gsw.Write(bs)
if err != nil {
return nil, err
}
err = gsw.Close()
if err != nil {
return nil, err
}
blob := gzbs.Bytes()
// Datastore has a max size of 1MB. If the blob is over 9.5MB, it probably
// won't fit after accounting for overhead.
if len(blob) > 950000 {
return nil, errTooBig{
fmt.Errorf("buildbotBuild: Build too big to store (%d bytes)", len(blob))}
}
p := datastore.PropertyMap{
"data": datastore.MkPropertyNI(blob),
}
if withMeta {
p["id"] = datastore.MkPropertyNI(b.getID())
p["master"] = datastore.MkProperty(b.Master)
p["builder"] = datastore.MkProperty(b.Buildername)
p["number"] = datastore.MkProperty(b.Number)
p["finished"] = datastore.MkProperty(b.Finished)
}
return p, nil
}
type buildbotPending struct {
Source buildbotSourceStamp `json:"source"`
Reason string `json:"reason"`
SubmittedAt int `json:"submittedAt"`
BuilderName string `json:"builderName"`
}
// buildbotBuilder is a builder struct from the master json, _not_ the builder json.
type buildbotBuilder struct {
Basedir string `json:"basedir"`
CachedBuilds []int `json:"cachedBuilds"`
PendingBuilds int `json:"pendingBuilds"`
// This one is specific to the pubsub interface. This is limited to 75,
// so it could differ from PendingBuilds
PendingBuildStates []*buildbotPending `json:"pendingBuildStates"`
Category string `json:"category"`
CurrentBuilds []int `json:"currentBuilds"`
Slaves []string `json:"slaves"`
State string `json:"state"`
}
// buildbotChangeSource is a changesource (ie polling source) usually tied to a master's scheduler.
type buildbotChangeSource struct {
Description string `json:"description"`
}
// buildbotChange describes a commit in a repository as part of a changesource of blamelist.
type buildbotChange struct {
At string `json:"at"`
Branch *string `json:"branch"`
Category string `json:"category"`
Comments string `json:"comments"`
// This could be a list of strings or list of struct { Name string } .
Files []interface{} `json:"files"`
Number int `json:"number"`
Project string `json:"project"`
Properties [][]interface{} `json:"properties"`
Repository string `json:"repository"`
Rev string `json:"rev"`
Revision string `json:"revision"`
Revlink string `json:"revlink"`
When int `json:"when"`
Who string `json:"who"`
}
func (bc *buildbotChange) GetFiles() []string {
files := make([]string, 0, len(bc.Files))
for _, f := range bc.Files {
// Buildbot stores files both as a string, or as a dict with a single entry
// named "name". It doesn't matter to us what the type is, but we need
// to reflect on the type anyways.
switch fn := f.(type) {
case string:
files = append(files, fn)
case map[string]interface{}:
if name, ok := fn["name"]; ok {
files = append(files, fmt.Sprintf("%s", name))
}
}
}
return files
}
// buildbotSlave describes a slave on a master from a master json, and also includes the
// full builds of any currently running builds.
type buildbotSlave struct {
// RecentBuilds is a map of builder name to a list of recent finished build
// numbers on that builder.
RecentBuilds map[string][]int `json:"builders"`
Connected bool `json:"connected"`
Host string `json:"host"`
Name string `json:"name"`
Runningbuilds []*buildbotBuild `json:"runningBuilds"`
Version string `json:"version"`
// This is like runningbuilds, but instead of storing the full build,
// just reference the build by builder: build num.
RunningbuildsMap map[string][]int `json:"runningBuildsMap"`
}
type buildbotProject struct {
BuildbotURL string `json:"buildbotURL"`
Title string `json:"title"`
Titleurl string `json:"titleURL"`
}
// buildbotMaster This is json definition for https://build.chromium.org/p/<master>/json
// endpoints.
type buildbotMaster struct {
AcceptingBuilds struct {
AcceptingBuilds bool `json:"accepting_builds"`
} `json:"accepting_builds"`
Builders map[string]*buildbotBuilder `json:"builders"`
Buildstate struct {
AcceptingBuilds bool `json:"accepting_builds"`
Builders []struct {
Basedir string `json:"basedir"`
Buildername string `json:"builderName"`
Cachedbuilds []int `json:"cachedBuilds"`
Category string `json:"category"`
Currentbuilds []int `json:"currentBuilds"`
Slaves []string `json:"slaves"`
State string `json:"state"`
} `json:"builders"`
Project struct {
BuildbotURL string `json:"buildbotURL"`
Title string `json:"title"`
Titleurl string `json:"titleURL"`
} `json:"project"`
Timestamp float64 `json:"timestamp"`
} `json:"buildstate"`
ChangeSources map[string]buildbotChangeSource `json:"change_sources"`
Changes map[string]buildbotChange `json:"changes"`
Clock struct {
Current struct {
Local string `json:"local"`
Utc string `json:"utc"`
UtcTs float64 `json:"utc_ts"`
} `json:"current"`
ServerStarted struct {
Local string `json:"local"`
Utc string `json:"utc"`
UtcTs float64 `json:"utc_ts"`
} `json:"server_started"`
ServerUptime float64 `json:"server_uptime"`
} `json:"clock"`
Project buildbotProject `json:"project"`
Slaves map[string]*buildbotSlave `json:"slaves"`
Varz struct {
AcceptingBuilds bool `json:"accepting_builds"`
Builders map[string]struct {
ConnectedSlaves int `json:"connected_slaves"`
CurrentBuilds int `json:"current_builds"`
PendingBuilds int `json:"pending_builds"`
State string `json:"state"`
TotalSlaves int `json:"total_slaves"`
} `json:"builders"`
ServerUptime float64 `json:"server_uptime"`
} `json:"varz"`
// This is injected by the pubsub publisher on the buildbot side.
Name string `json:"name"`
}