| OLD | NEW |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. | 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package buildbucket | 5 package buildbucket |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "encoding/json" | 8 "encoding/json" |
| 9 "fmt" | 9 "fmt" |
| 10 "net/url" | 10 "net/url" |
| 11 "os" | 11 "os" |
| 12 "path" | 12 "path" |
| 13 "path/filepath" | 13 "path/filepath" |
| 14 "strconv" | 14 "strconv" |
| 15 "strings" | 15 "strings" |
| 16 "time" | 16 "time" |
| 17 | 17 |
| 18 "golang.org/x/net/context" | 18 "golang.org/x/net/context" |
| 19 "google.golang.org/api/googleapi" | 19 "google.golang.org/api/googleapi" |
| 20 | 20 |
| 21 "github.com/luci/luci-go/common/api/buildbucket/buildbucket/v1" | 21 "github.com/luci/luci-go/common/api/buildbucket/buildbucket/v1" |
| 22 "github.com/luci/luci-go/common/clock" | 22 "github.com/luci/luci-go/common/clock" |
| 23 "github.com/luci/luci-go/common/errors" | 23 "github.com/luci/luci-go/common/errors" |
| 24 » log "github.com/luci/luci-go/common/logging" | 24 » "github.com/luci/luci-go/common/logging" |
| 25 "github.com/luci/luci-go/common/retry" | 25 "github.com/luci/luci-go/common/retry" |
| 26 "github.com/luci/luci-go/common/sync/parallel" | 26 "github.com/luci/luci-go/common/sync/parallel" |
| 27 "github.com/luci/luci-go/milo/api/resp" | 27 "github.com/luci/luci-go/milo/api/resp" |
| 28 "github.com/luci/luci-go/milo/appengine/common" |
| 28 ) | 29 ) |
| 29 | 30 |
| 30 // search executes the search request with retries and exponential back-off. | 31 // search executes the search request with retries and exponential back-off. |
| 31 func search(c context.Context, client *buildbucket.Service, req *buildbucket.Sea
rchCall) ( | 32 func search(c context.Context, client *buildbucket.Service, req *buildbucket.Sea
rchCall) ( |
| 32 *buildbucket.ApiSearchResponseMessage, error) { | 33 *buildbucket.ApiSearchResponseMessage, error) { |
| 33 | 34 |
| 34 var res *buildbucket.ApiSearchResponseMessage | 35 var res *buildbucket.ApiSearchResponseMessage |
| 35 err := retry.Retry( | 36 err := retry.Retry( |
| 36 c, | 37 c, |
| 37 retry.TransientOnly(retry.Default), | 38 retry.TransientOnly(retry.Default), |
| 38 func() error { | 39 func() error { |
| 39 var err error | 40 var err error |
| 40 res, err = req.Do() | 41 res, err = req.Do() |
| 41 if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Co
de >= 500 { | 42 if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Co
de >= 500 { |
| 42 err = errors.WrapTransient(apiErr) | 43 err = errors.WrapTransient(apiErr) |
| 43 } | 44 } |
| 44 return err | 45 return err |
| 45 }, | 46 }, |
| 46 func(err error, wait time.Duration) { | 47 func(err error, wait time.Duration) { |
| 47 » » » log.WithError(err).Warningf(c, "buildbucket search reque
st failed transiently, will retry in %s", wait) | 48 » » » logging.WithError(err).Warningf(c, "buildbucket search r
equest failed transiently, will retry in %s", wait) |
| 48 }) | 49 }) |
| 49 return res, err | 50 return res, err |
| 50 } | 51 } |
| 51 | 52 |
| 52 // fetchBuilds fetches builds given a criteria. | 53 // fetchBuilds fetches builds given a criteria. |
| 53 // The returned builds are sorted by build creation descending. | 54 // The returned builds are sorted by build creation descending. |
| 54 // count defines maximum number of builds to fetch; if <0, defaults to 100. | 55 // count defines maximum number of builds to fetch; if <0, defaults to 100. |
| 55 func fetchBuilds(c context.Context, client *buildbucket.Service, bucket, builder
, | 56 func fetchBuilds(c context.Context, client *buildbucket.Service, bucket, builder
, |
| 56 status string, count int) ([]*buildbucket.ApiBuildMessage, error) { | 57 status string, count int) ([]*buildbucket.ApiBuildMessage, error) { |
| 57 | 58 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 77 return fetched, fmt.Errorf(res.Error.Message) | 78 return fetched, fmt.Errorf(res.Error.Message) |
| 78 } | 79 } |
| 79 | 80 |
| 80 fetched = append(fetched, res.Builds...) | 81 fetched = append(fetched, res.Builds...) |
| 81 | 82 |
| 82 if len(res.Builds) == 0 || res.NextCursor == "" { | 83 if len(res.Builds) == 0 || res.NextCursor == "" { |
| 83 break | 84 break |
| 84 } | 85 } |
| 85 req.StartCursor(res.NextCursor) | 86 req.StartCursor(res.NextCursor) |
| 86 } | 87 } |
| 87 » log.Debugf(c, "Fetched %d %s builds in %s", len(fetched), status, clock.
Since(c, start)) | 88 » logging.Debugf(c, "Fetched %d %s builds in %s", len(fetched), status, cl
ock.Since(c, start)) |
| 88 return fetched, nil | 89 return fetched, nil |
| 89 } | 90 } |
| 90 | 91 |
| 91 // toMiloBuild converts a buildbucket build to a milo build. | 92 // toMiloBuild converts a buildbucket build to a milo build. |
| 92 // In case of an error, returns a build with a description of the error | 93 // In case of an error, returns a build with a description of the error |
| 93 // and logs the error. | 94 // and logs the error. |
| 94 func toMiloBuild(c context.Context, build *buildbucket.ApiBuildMessage) *resp.Bu
ildSummary { | 95 func toMiloBuild(c context.Context, build *buildbucket.ApiBuildMessage) *resp.Bu
ildSummary { |
| 95 // Parsing of parameters and result details is best effort. | 96 // Parsing of parameters and result details is best effort. |
| 96 var params buildParameters | 97 var params buildParameters |
| 97 if err := json.NewDecoder(strings.NewReader(build.ParametersJson)).Decod
e(¶ms); err != nil { | 98 if err := json.NewDecoder(strings.NewReader(build.ParametersJson)).Decod
e(¶ms); err != nil { |
| 98 » » log.Errorf(c, "Could not parse parameters of build %d: %s", buil
d.Id, err) | 99 » » logging.Errorf(c, "Could not parse parameters of build %d: %s",
build.Id, err) |
| 99 } | 100 } |
| 100 var resultDetails resultDetails | 101 var resultDetails resultDetails |
| 101 if err := json.NewDecoder(strings.NewReader(build.ResultDetailsJson)).De
code(&resultDetails); err != nil { | 102 if err := json.NewDecoder(strings.NewReader(build.ResultDetailsJson)).De
code(&resultDetails); err != nil { |
| 102 » » log.Errorf(c, "Could not parse result details of build %d: %s",
build.Id, err) | 103 » » logging.Errorf(c, "Could not parse result details of build %d: %
s", build.Id, err) |
| 103 } | 104 } |
| 104 | 105 |
| 105 result := &resp.BuildSummary{ | 106 result := &resp.BuildSummary{ |
| 106 Text: []string{fmt.Sprintf("buildbucket id %d", build.Id)}, | 107 Text: []string{fmt.Sprintf("buildbucket id %d", build.Id)}, |
| 107 Revision: resultDetails.Properties.GotRevision, | 108 Revision: resultDetails.Properties.GotRevision, |
| 108 } | 109 } |
| 109 if result.Revision == "" { | 110 if result.Revision == "" { |
| 110 result.Revision = params.Properties.Revision | 111 result.Revision = params.Properties.Revision |
| 111 } | 112 } |
| 112 | 113 |
| 113 var err error | 114 var err error |
| 114 result.Status, err = parseStatus(build) | 115 result.Status, err = parseStatus(build) |
| 115 if err != nil { | 116 if err != nil { |
| 116 // almost never happens | 117 // almost never happens |
| 117 » » log.WithError(err).Errorf(c, "could not convert status of build
%d", build.Id) | 118 » » logging.WithError(err).Errorf(c, "could not convert status of bu
ild %d", build.Id) |
| 118 result.Status = resp.InfraFailure | 119 result.Status = resp.InfraFailure |
| 119 result.Text = append(result.Text, fmt.Sprintf("invalid build: %s
", err)) | 120 result.Text = append(result.Text, fmt.Sprintf("invalid build: %s
", err)) |
| 120 } | 121 } |
| 121 | 122 |
| 122 result.PendingTime.Started = parseTimestamp(build.CreatedTs) | 123 result.PendingTime.Started = parseTimestamp(build.CreatedTs) |
| 123 switch build.Status { | 124 switch build.Status { |
| 124 case "SCHEDULED": | 125 case "SCHEDULED": |
| 125 result.PendingTime.Duration = clock.Since(c, result.PendingTime.
Started) | 126 result.PendingTime.Duration = clock.Since(c, result.PendingTime.
Started) |
| 126 | 127 |
| 127 case "STARTED": | 128 case "STARTED": |
| (...skipping 14 matching lines...) Expand all Loading... |
| 142 | 143 |
| 143 tags := ParseTags(build.Tags) | 144 tags := ParseTags(build.Tags) |
| 144 | 145 |
| 145 if build.Url != "" { | 146 if build.Url != "" { |
| 146 u := build.Url | 147 u := build.Url |
| 147 parsed, err := url.Parse(u) | 148 parsed, err := url.Parse(u) |
| 148 | 149 |
| 149 // map milo links to itself | 150 // map milo links to itself |
| 150 switch { | 151 switch { |
| 151 case err != nil: | 152 case err != nil: |
| 152 » » » log.Errorf(c, "invalid URL in build %d: %s", build.Id, e
rr) | 153 » » » logging.Errorf(c, "invalid URL in build %d: %s", build.I
d, err) |
| 153 case parsed.Host == "luci-milo.appspot.com": | 154 case parsed.Host == "luci-milo.appspot.com": |
| 154 parsed.Host = "" | 155 parsed.Host = "" |
| 155 parsed.Scheme = "" | 156 parsed.Scheme = "" |
| 156 u = parsed.String() | 157 u = parsed.String() |
| 157 } | 158 } |
| 158 | 159 |
| 159 result.Link = &resp.Link{ | 160 result.Link = &resp.Link{ |
| 160 URL: u, | 161 URL: u, |
| 161 Label: strconv.FormatInt(build.Id, 10), | 162 Label: strconv.FormatInt(build.Id, 10), |
| 162 } | 163 } |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 213 } | 214 } |
| 214 | 215 |
| 215 default: | 216 default: |
| 216 panic("impossible") | 217 panic("impossible") |
| 217 } | 218 } |
| 218 } | 219 } |
| 219 return nil | 220 return nil |
| 220 } | 221 } |
| 221 | 222 |
| 222 type builderQuery struct { | 223 type builderQuery struct { |
| 223 Server string | |
| 224 Bucket string | 224 Bucket string |
| 225 Builder string | 225 Builder string |
| 226 Limit int | 226 Limit int |
| 227 } | 227 } |
| 228 | 228 |
| 229 // builderImpl is the implementation for getting a milo builder page from buildb
ucket. | 229 // builderImpl is the implementation for getting a milo builder page from buildb
ucket. |
| 230 // if maxCompletedBuilds < 0, 25 is used. | 230 // if maxCompletedBuilds < 0, 25 is used. |
| 231 func builderImpl(c context.Context, q builderQuery) (*resp.Builder, error) { | 231 func builderImpl(c context.Context, q builderQuery) (*resp.Builder, error) { |
| 232 settings, err := common.GetSettings(c) |
| 233 if err != nil { |
| 234 logging.WithError(err).Errorf(c, "failed to get settings") |
| 235 return nil, err |
| 236 } |
| 237 if settings.Buildbucket == nil || settings.Buildbucket.Host == "" { |
| 238 logging.WithError(err).Errorf(c, "missing buildbucket settings") |
| 239 return nil, errors.New("missing buildbucket settings") |
| 240 } |
| 241 host := settings.Buildbucket.Host |
| 242 |
| 232 if q.Limit < 0 { | 243 if q.Limit < 0 { |
| 233 q.Limit = 20 | 244 q.Limit = 20 |
| 234 } | 245 } |
| 235 | 246 |
| 236 result := &resp.Builder{ | 247 result := &resp.Builder{ |
| 237 Name: q.Builder, | 248 Name: q.Builder, |
| 238 } | 249 } |
| 239 » if q.Server == "debug" { | 250 » if host == "debug" { |
| 240 return result, getDebugBuilds(c, q.Bucket, q.Builder, q.Limit, r
esult) | 251 return result, getDebugBuilds(c, q.Bucket, q.Builder, q.Limit, r
esult) |
| 241 } | 252 } |
| 242 » client, err := newBuildbucketClient(c, q.Server) | 253 » client, err := newBuildbucketClient(c, host) |
| 243 if err != nil { | 254 if err != nil { |
| 244 return nil, err | 255 return nil, err |
| 245 } | 256 } |
| 246 | 257 |
| 247 fetch := func(target *[]*resp.BuildSummary, status string, count int) er
ror { | 258 fetch := func(target *[]*resp.BuildSummary, status string, count int) er
ror { |
| 248 builds, err := fetchBuilds(c, client, q.Bucket, q.Builder, statu
s, count) | 259 builds, err := fetchBuilds(c, client, q.Bucket, q.Builder, statu
s, count) |
| 249 if err != nil { | 260 if err != nil { |
| 250 » » » log.Errorf(c, "Could not fetch builds with status %s: %s
", status, err) | 261 » » » logging.Errorf(c, "Could not fetch builds with status %s
: %s", status, err) |
| 251 return err | 262 return err |
| 252 } | 263 } |
| 253 *target = make([]*resp.BuildSummary, len(builds)) | 264 *target = make([]*resp.BuildSummary, len(builds)) |
| 254 for i, bb := range builds { | 265 for i, bb := range builds { |
| 255 (*target)[i] = toMiloBuild(c, bb) | 266 (*target)[i] = toMiloBuild(c, bb) |
| 256 } | 267 } |
| 257 return nil | 268 return nil |
| 258 } | 269 } |
| 259 // fetch pending, current and finished builds concurrently. | 270 // fetch pending, current and finished builds concurrently. |
| 260 // Why not a single request? Because we need different build number | 271 // Why not a single request? Because we need different build number |
| (...skipping 19 matching lines...) Expand all Loading... |
| 280 return time.Unix(microseconds/1e6, microseconds%1e6*1000).UTC() | 291 return time.Unix(microseconds/1e6, microseconds%1e6*1000).UTC() |
| 281 } | 292 } |
| 282 | 293 |
| 283 type newBuildsFirst []*resp.BuildSummary | 294 type newBuildsFirst []*resp.BuildSummary |
| 284 | 295 |
| 285 func (a newBuildsFirst) Len() int { return len(a) } | 296 func (a newBuildsFirst) Len() int { return len(a) } |
| 286 func (a newBuildsFirst) Swap(i, j int) { a[i], a[j] = a[j], a[i] } | 297 func (a newBuildsFirst) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| 287 func (a newBuildsFirst) Less(i, j int) bool { | 298 func (a newBuildsFirst) Less(i, j int) bool { |
| 288 return a[i].PendingTime.Started.After(a[j].PendingTime.Started) | 299 return a[i].PendingTime.Started.After(a[j].PendingTime.Started) |
| 289 } | 300 } |
| OLD | NEW |