Chromium Code Reviews| 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.CodeParameterError).Err() | |
|
Ryan Tseng
2017/07/13 22:00:46
404?
iannucci
2017/07/14 19:00:22
oh yeah
| |
| 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 |