Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 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 swarming | 5 package swarming |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "bytes" | 8 "bytes" |
| 9 "encoding/json" | 9 "encoding/json" |
| 10 "fmt" | 10 "fmt" |
| 11 "io/ioutil" | 11 "io/ioutil" |
| 12 "path/filepath" | 12 "path/filepath" |
| 13 "strings" | 13 "strings" |
| 14 "sync" | 14 "sync" |
| 15 "time" | 15 "time" |
| 16 | 16 |
| 17 "github.com/luci/luci-go/appengine/cmd/milo/logdog" | 17 "github.com/luci/luci-go/appengine/cmd/milo/logdog" |
| 18 "github.com/luci/luci-go/appengine/cmd/milo/resp" | 18 "github.com/luci/luci-go/appengine/cmd/milo/resp" |
| 19 "github.com/luci/luci-go/appengine/gaeauth/client" | 19 "github.com/luci/luci-go/appengine/gaeauth/client" |
| 20 "github.com/luci/luci-go/client/logdog/annotee" | 20 "github.com/luci/luci-go/client/logdog/annotee" |
| 21 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" | 21 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" |
| 22 "github.com/luci/luci-go/common/clock" | |
| 22 "github.com/luci/luci-go/common/logdog/types" | 23 "github.com/luci/luci-go/common/logdog/types" |
| 23 "github.com/luci/luci-go/common/logging" | 24 "github.com/luci/luci-go/common/logging" |
| 24 miloProto "github.com/luci/luci-go/common/proto/milo" | 25 miloProto "github.com/luci/luci-go/common/proto/milo" |
| 25 "github.com/luci/luci-go/common/transport" | 26 "github.com/luci/luci-go/common/transport" |
| 26 "golang.org/x/net/context" | 27 "golang.org/x/net/context" |
| 27 ) | 28 ) |
| 28 | 29 |
| 29 // Swarming task states.. | 30 // Swarming task states.. |
| 30 const ( | 31 const ( |
| 31 // TaskRunning means task is running. | 32 // TaskRunning means task is running. |
| (...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 226 | 227 |
| 227 // Timeswamapts | 228 // Timeswamapts |
| 228 comp.Started = asc.Started.Time().Format(time.RFC3339) | 229 comp.Started = asc.Started.Time().Format(time.RFC3339) |
| 229 | 230 |
| 230 // This should be the exact same thing. | 231 // This should be the exact same thing. |
| 231 comp.Text = asc.Text | 232 comp.Text = asc.Text |
| 232 | 233 |
| 233 return comp | 234 return comp |
| 234 } | 235 } |
| 235 | 236 |
| 236 func swarmingProperties(sr *swarming.SwarmingRpcsTaskResult) *resp.PropertyGroup { | 237 func taskProperties(sr *swarming.SwarmingRpcsTaskResult) *resp.PropertyGroup { |
| 237 props := &resp.PropertyGroup{GroupName: "Swarming"} | 238 props := &resp.PropertyGroup{GroupName: "Swarming"} |
| 238 if len(sr.CostsUsd) == 1 { | 239 if len(sr.CostsUsd) == 1 { |
| 239 props.Property = append(props.Property, &resp.Property{ | 240 props.Property = append(props.Property, &resp.Property{ |
| 240 Key: "Cost of job (USD)", | 241 Key: "Cost of job (USD)", |
| 241 Value: fmt.Sprintf("$%.2f", sr.CostsUsd[0]), | 242 Value: fmt.Sprintf("$%.2f", sr.CostsUsd[0]), |
| 242 }) | 243 }) |
| 243 } | 244 } |
| 244 if sr.State == TaskCompleted || sr.State == TaskTimedOut { | 245 if sr.State == TaskCompleted || sr.State == TaskTimedOut { |
| 245 props.Property = append(props.Property, &resp.Property{ | 246 props.Property = append(props.Property, &resp.Property{ |
| 246 Key: "Exit Code", | 247 Key: "Exit Code", |
| 247 Value: fmt.Sprintf("%d", sr.ExitCode), | 248 Value: fmt.Sprintf("%d", sr.ExitCode), |
| 248 }) | 249 }) |
| 249 } | 250 } |
| 250 return props | 251 return props |
| 251 } | 252 } |
| 252 | 253 |
| 253 func swarmingTags(sr *swarming.SwarmingRpcsTaskResult) *resp.PropertyGroup { | 254 func tagsToProperties(tags []string) *resp.PropertyGroup { |
| 254 props := &resp.PropertyGroup{GroupName: "Swarming Tags"} | 255 props := &resp.PropertyGroup{GroupName: "Swarming Tags"} |
| 255 » for _, s := range sr.Tags { | 256 » for _, t := range tags { |
| 256 » » sp := strings.SplitN(s, ":", 2) | 257 » » if t == "" { |
|
estaab
2016/06/16 20:13:45
What does an empty tag mean?
nodir
2016/06/16 21:08:04
we should not have empty tags, but without this if
| |
| 257 » » var k, v string | 258 » » » continue |
| 258 » » k = sp[0] | |
| 259 » » if len(sp) == 2 { | |
| 260 » » » v = sp[1] | |
| 261 } | 259 } |
| 262 » » props.Property = append(props.Property, &resp.Property{ | 260 » » parts := strings.SplitN(t, ":", 2) |
| 263 » » » Key: k, | 261 » » p := &resp.Property{ |
| 264 » » » Value: v, | 262 » » » Key: parts[0], |
| 265 » » }) | 263 » » } |
| 264 » » if len(parts) == 2 { | |
| 265 » » » p.Value = parts[1] | |
| 266 » » } | |
| 267 » » props.Property = append(props.Property, p) | |
| 266 } | 268 } |
| 267 return props | 269 return props |
| 268 } | 270 } |
| 269 | 271 |
| 270 func addSwarmingToBuild( | 272 func taskToBuild(c context.Context, sr *swarming.SwarmingRpcsTaskResult) (*resp. MiloBuild, error) { |
| 271 » c context.Context, sr *swarming.SwarmingRpcsTaskResult, build *resp.Milo Build) { | 273 » build := &resp.MiloBuild{} |
| 272 » // Specify the result. | 274 » switch sr.State { |
| 273 » if sr.State == "RUNNING" { | 275 » case TaskRunning: |
|
estaab
2016/06/16 20:13:45
nice, much better with these constants.
| |
| 274 build.Summary.Status = resp.Running | 276 build.Summary.Status = resp.Running |
| 275 » } else if sr.State == "PENDING" { | 277 |
| 278 » case TaskPending: | |
| 276 build.Summary.Status = resp.NotRun | 279 build.Summary.Status = resp.NotRun |
| 277 » } else if sr.InternalFailure == true || sr.State == "BOT_DIED" || sr.Sta te == "EXPIRED" || sr.State == "TIMED_OUT" { | 280 |
| 281 » case TaskExpired, TaskTimedOut, TaskBotDied: | |
| 278 build.Summary.Status = resp.InfraFailure | 282 build.Summary.Status = resp.InfraFailure |
| 279 » } else if sr.Failure == true || sr.State == "CANCELLED" { | 283 |
| 284 » case TaskCanceled: | |
| 280 // Cancelled build is user action, so it is not an infra failure . | 285 // Cancelled build is user action, so it is not an infra failure . |
| 281 build.Summary.Status = resp.Failure | 286 build.Summary.Status = resp.Failure |
| 282 » } else { | 287 |
| 283 » » build.Summary.Status = resp.Success | 288 » case TaskCompleted: |
| 289 | |
| 290 » » switch { | |
| 291 » » case sr.InternalFailure: | |
| 292 » » » build.Summary.Status = resp.InfraFailure | |
| 293 » » case sr.Failure: | |
| 294 » » » build.Summary.Status = resp.Failure | |
| 295 » » default: | |
| 296 » » » build.Summary.Status = resp.Success | |
| 297 » » } | |
| 298 | |
| 299 » default: | |
| 300 » » return nil, fmt.Errorf("unknown task state %q", sr.State) | |
|
Ryan Tseng
2016/06/16 20:21:13
unknown swarming task state
| |
| 284 } | 301 } |
| 285 | 302 |
| 286 // Extract more swarming specific information into the properties. | 303 // Extract more swarming specific information into the properties. |
| 287 » build.PropertyGroup = append(build.PropertyGroup, swarmingProperties(sr) ) | 304 » if props := taskProperties(sr); len(props.Property) > 0 { |
|
Ryan Tseng
2016/06/16 20:21:13
nit: Not actually sure if the if statement are nec
nodir
2016/06/16 21:08:04
it avoids adding property groups that don't have p
| |
| 288 » build.PropertyGroup = append(build.PropertyGroup, swarmingTags(sr)) | 305 » » build.PropertyGroup = append(build.PropertyGroup, props) |
| 306 » } | |
| 307 » if props := tagsToProperties(sr.Tags); len(props.Property) > 0 { | |
| 308 » » build.PropertyGroup = append(build.PropertyGroup, props) | |
| 309 » } | |
| 289 | 310 |
| 290 » // Build times. Swarming timestamps are RFC3339Nano without the timezon e | 311 » // Build times. Swarming timestamps are UTC RFC3339Nano, but without the |
| 291 » // information, which is assumed to be UTC, so we fix it here. | 312 » // timezone information,. Make them valid RFC3339Nano. |
|
estaab
2016/06/16 20:13:45
extra comma
nodir
2016/06/16 21:08:04
Done.
| |
| 292 » build.Summary.Started = fmt.Sprintf("%sZ", sr.StartedTs) | 313 » build.Summary.Started = sr.StartedTs + "Z" |
| 293 if sr.CompletedTs != "" { | 314 if sr.CompletedTs != "" { |
| 294 » » build.Summary.Finished = fmt.Sprintf("%sZ", sr.CompletedTs) | 315 » » build.Summary.Finished = sr.CompletedTs + "Z" |
| 295 } | 316 } |
| 296 » build.Summary.Duration = uint64(sr.Duration) | 317 » if sr.Duration != 0 { |
| 318 » » build.Summary.Duration = uint64(sr.Duration) | |
| 319 » } else if sr.State == TaskRunning { | |
| 320 » » started, err := time.Parse(time.RFC3339, build.Summary.Started) | |
| 321 » » if err != nil { | |
| 322 » » » return nil, fmt.Errorf("invalid task StartedTs: %s", err ) | |
| 323 » » } | |
| 324 » » now := clock.Now(c) | |
| 325 » » if started.Before(now) { | |
| 326 » » » build.Summary.Duration = uint64(clock.Now(c).Sub(started ).Seconds()) | |
| 327 » » } | |
| 328 » } | |
| 329 | |
| 330 » return build, nil | |
| 297 } | 331 } |
| 298 | 332 |
| 299 // Takes in an annotated log and returns a fully populated set of logdog streams | 333 // streamsFromAnnotatedLog takes in an annotated log and returns a fully |
| 334 // populated set of logdog streams | |
| 300 func streamsFromAnnotatedLog(ctx context.Context, log []byte) (*logdog.Streams, error) { | 335 func streamsFromAnnotatedLog(ctx context.Context, log []byte) (*logdog.Streams, error) { |
| 301 c := &memoryClient{} | 336 c := &memoryClient{} |
| 302 p := annotee.New(ctx, annotee.Options{ | 337 p := annotee.New(ctx, annotee.Options{ |
| 303 Client: c, | 338 Client: c, |
| 304 MetadataUpdateInterval: -1, // Neverrrrrr send incr updates. | 339 MetadataUpdateInterval: -1, // Neverrrrrr send incr updates. |
| 305 Offline: true, | 340 Offline: true, |
| 306 }) | 341 }) |
| 307 defer p.Finish() | |
| 308 | 342 |
| 309 is := annotee.Stream{ | 343 is := annotee.Stream{ |
| 310 Reader: bytes.NewBuffer(log), | 344 Reader: bytes.NewBuffer(log), |
| 311 Name: types.StreamName("stdout"), | 345 Name: types.StreamName("stdout"), |
| 312 Annotate: true, | 346 Annotate: true, |
| 313 StripAnnotations: true, | 347 StripAnnotations: true, |
| 314 } | 348 } |
| 315 // If this ever has more than one stream then memoryClient needs to beco me | 349 // If this ever has more than one stream then memoryClient needs to beco me |
| 316 // goroutine safe | 350 // goroutine safe |
| 317 if err := p.RunStreams([]*annotee.Stream{&is}); err != nil { | 351 if err := p.RunStreams([]*annotee.Stream{&is}); err != nil { |
| 318 return nil, err | 352 return nil, err |
| 319 } | 353 } |
| 320 » p.Finish() | 354 » p.Finish(false) |
| 321 return c.ToLogDogStreams() | 355 return c.ToLogDogStreams() |
| 322 } | 356 } |
| 323 | 357 |
| 324 func swarmingBuildImpl(c context.Context, URL string, server string, taskID stri ng) (*resp.MiloBuild, error) { | 358 func swarmingBuildImpl(c context.Context, URL string, server string, taskID stri ng) (*resp.MiloBuild, error) { |
| 325 // Fetch the data from Swarming | 359 // Fetch the data from Swarming |
| 326 sr, body, err := getSwarming(c, server, taskID) | 360 sr, body, err := getSwarming(c, server, taskID) |
| 327 if err != nil { | 361 if err != nil { |
| 328 return nil, err | 362 return nil, err |
| 329 } | 363 } |
| 330 | 364 |
| 331 allowMilo := false | 365 allowMilo := false |
| 332 for _, t := range sr.Tags { | 366 for _, t := range sr.Tags { |
| 333 if t == "allow_milo:1" { | 367 if t == "allow_milo:1" { |
| 334 allowMilo = true | 368 allowMilo = true |
| 335 break | 369 break |
| 336 } | 370 } |
| 337 } | 371 } |
| 338 if !allowMilo { | 372 if !allowMilo { |
| 339 return nil, fmt.Errorf("Not A Milo Job") | 373 return nil, fmt.Errorf("Not A Milo Job") |
| 340 } | 374 } |
| 341 | 375 |
| 342 » build := &resp.MiloBuild{} | 376 » build, err := taskToBuild(c, sr) |
| 343 » addSwarmingToBuild(c, sr, build) | 377 » if err != nil { |
| 378 » » return nil, err | |
| 379 » } | |
| 344 | 380 |
| 345 // Decode the data using annotee. The logdog stream returned here is ass umed | 381 // Decode the data using annotee. The logdog stream returned here is ass umed |
| 346 // to be consistent, which is why the following block of code are not | 382 // to be consistent, which is why the following block of code are not |
| 347 // expected to ever err out. | 383 // expected to ever err out. |
| 348 lds, err := streamsFromAnnotatedLog(c, body) | 384 lds, err := streamsFromAnnotatedLog(c, body) |
| 349 if err != nil { | 385 if err != nil { |
| 350 build.Components = []*resp.BuildComponent{{ | 386 build.Components = []*resp.BuildComponent{{ |
| 351 Type: resp.Summary, | 387 Type: resp.Summary, |
| 352 Label: "milo annotation parser", | 388 Label: "milo annotation parser", |
| 353 Text: []string{err.Error()}, | 389 Text: []string{err.Error()}, |
| 354 Status: resp.InfraFailure, | 390 Status: resp.InfraFailure, |
| 355 SubLink: []*resp.Link{{ | 391 SubLink: []*resp.Link{{ |
| 356 Label: "swarming task", | 392 Label: "swarming task", |
| 357 URL: taskPageURL(resolveServer(server), taskID ), | 393 URL: taskPageURL(resolveServer(server), taskID ), |
| 358 }}, | 394 }}, |
| 359 }} | 395 }} |
| 360 } else { | 396 } else { |
| 361 logdog.AddLogDogToBuild(c, URL, lds, build) | 397 logdog.AddLogDogToBuild(c, URL, lds, build) |
| 362 } | 398 } |
| 363 | 399 |
| 364 return build, nil | 400 return build, nil |
| 365 } | 401 } |
| 366 | 402 |
| 367 // taskPageURL returns a URL to a human-consumable page of a swarming task. | 403 // taskPageURL returns a URL to a human-consumable page of a swarming task. |
| 368 func taskPageURL(swarmingHostname, taskID string) string { | 404 func taskPageURL(swarmingHostname, taskID string) string { |
| 369 return fmt.Sprintf("https://%s/user/task/%s", swarmingHostname, taskID) | 405 return fmt.Sprintf("https://%s/user/task/%s", swarmingHostname, taskID) |
| 370 } | 406 } |
| OLD | NEW |