| 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 "net/url" |
| 12 "path/filepath" | 13 "path/filepath" |
| 13 "strings" | 14 "strings" |
| 14 "sync" | 15 "sync" |
| 15 "time" | 16 "time" |
| 16 | 17 |
| 17 "golang.org/x/net/context" | 18 "golang.org/x/net/context" |
| 18 | 19 |
| 19 "github.com/luci/luci-go/appengine/cmd/milo/logdog" | 20 "github.com/luci/luci-go/appengine/cmd/milo/logdog" |
| 20 "github.com/luci/luci-go/appengine/cmd/milo/resp" | 21 "github.com/luci/luci-go/appengine/cmd/milo/resp" |
| 21 "github.com/luci/luci-go/appengine/gaeauth/client" | 22 "github.com/luci/luci-go/appengine/gaeauth/client" |
| 22 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" | 23 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" |
| 23 "github.com/luci/luci-go/common/clock" | |
| 24 "github.com/luci/luci-go/common/logging" | 24 "github.com/luci/luci-go/common/logging" |
| 25 "github.com/luci/luci-go/common/proto/google" |
| 26 miloProto "github.com/luci/luci-go/common/proto/milo" |
| 25 "github.com/luci/luci-go/common/transport" | 27 "github.com/luci/luci-go/common/transport" |
| 26 "github.com/luci/luci-go/logdog/client/annotee" | 28 "github.com/luci/luci-go/logdog/client/annotee" |
| 27 "github.com/luci/luci-go/logdog/common/types" | 29 "github.com/luci/luci-go/logdog/common/types" |
| 28 ) | 30 ) |
| 29 | 31 |
| 30 // SwarmingTimeLayout is time layout used by swarming. | 32 // SwarmingTimeLayout is time layout used by swarming. |
| 31 const SwarmingTimeLayout = "2006-01-02T15:04:05.999999999" | 33 const SwarmingTimeLayout = "2006-01-02T15:04:05.999999999" |
| 32 | 34 |
| 33 // Swarming task states.. | 35 // Swarming task states.. |
| 34 const ( | 36 const ( |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 251 | 253 |
| 252 build.Summary.Banner = &resp.LogoBanner{ | 254 build.Summary.Banner = &resp.LogoBanner{ |
| 253 OS: []resp.Logo{{ | 255 OS: []resp.Logo{{ |
| 254 LogoBase: base, | 256 LogoBase: base, |
| 255 Subtitle: ver, | 257 Subtitle: ver, |
| 256 Count: 1, | 258 Count: 1, |
| 257 }}, | 259 }}, |
| 258 } | 260 } |
| 259 } | 261 } |
| 260 | 262 |
| 261 func taskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsTask
Result) (*resp.MiloBuild, error) { | 263 // addTaskToMiloStep augments a Milo Annotation Protobuf with state from the |
| 262 » build := &resp.MiloBuild{ | 264 // Swarming task. |
| 263 » » Summary: resp.BuildComponent{ | 265 func addTaskToMiloStep(c context.Context, server string, sr *swarming.SwarmingRp
csTaskResult, step *miloProto.Step) error { |
| 264 » » » Label: sr.TaskId, | 266 » step.Link = &miloProto.Link{ |
| 265 » » » Source: &resp.Link{ | 267 » » Label: "Task " + sr.TaskId, |
| 266 » » » » Label: "Task " + sr.TaskId, | 268 » » Value: &miloProto.Link_Url{ |
| 267 » » » » URL: taskPageURL(server, sr.TaskId), | 269 » » » Url: taskPageURL(server, sr.TaskId), |
| 268 » » » }, | |
| 269 }, | 270 }, |
| 270 } | 271 } |
| 271 | 272 |
| 272 switch sr.State { | 273 switch sr.State { |
| 273 case TaskRunning: | 274 case TaskRunning: |
| 274 » » build.Summary.Status = resp.Running | 275 » » step.Status = miloProto.Status_RUNNING |
| 275 | 276 |
| 276 case TaskPending: | 277 case TaskPending: |
| 277 » » build.Summary.Status = resp.NotRun | 278 » » step.Status = miloProto.Status_PENDING |
| 278 | 279 |
| 279 case TaskExpired, TaskTimedOut, TaskBotDied: | 280 case TaskExpired, TaskTimedOut, TaskBotDied: |
| 280 » » build.Summary.Status = resp.InfraFailure | 281 » » step.Status = miloProto.Status_FAILURE |
| 282 » » step.FailureDetails = &miloProto.FailureDetails{ |
| 283 » » » Type: miloProto.FailureDetails_INFRA, |
| 284 » » } |
| 285 |
| 281 switch sr.State { | 286 switch sr.State { |
| 282 case TaskExpired: | 287 case TaskExpired: |
| 283 » » » build.Summary.Text = append(build.Summary.Text, "Task ex
pired") | 288 » » » step.FailureDetails.Text = "Task expired" |
| 284 case TaskTimedOut: | 289 case TaskTimedOut: |
| 285 » » » build.Summary.Text = append(build.Summary.Text, "Task ti
med out") | 290 » » » step.FailureDetails.Text = "Task timed out" |
| 286 case TaskBotDied: | 291 case TaskBotDied: |
| 287 » » » build.Summary.Text = append(build.Summary.Text, "Bot die
d") | 292 » » » step.FailureDetails.Text = "Bot died" |
| 288 } | 293 } |
| 289 | 294 |
| 290 case TaskCanceled: | 295 case TaskCanceled: |
| 291 // Cancelled build is user action, so it is not an infra failure
. | 296 // Cancelled build is user action, so it is not an infra failure
. |
| 292 » » build.Summary.Status = resp.Failure | 297 » » step.Status = miloProto.Status_FAILURE |
| 293 » » build.Summary.Text = append(build.Summary.Text, "Task cancelled
by user") | 298 » » step.FailureDetails = &miloProto.FailureDetails{ |
| 299 » » » Type: miloProto.FailureDetails_CANCELLED, |
| 300 » » » Text: "Task cancelled by user", |
| 301 » » } |
| 294 | 302 |
| 295 case TaskCompleted: | 303 case TaskCompleted: |
| 296 | 304 |
| 297 switch { | 305 switch { |
| 298 case sr.InternalFailure: | 306 case sr.InternalFailure: |
| 299 » » » build.Summary.Status = resp.InfraFailure | 307 » » » step.Status = miloProto.Status_FAILURE |
| 308 » » » step.FailureDetails = &miloProto.FailureDetails{ |
| 309 » » » » Type: miloProto.FailureDetails_INFRA, |
| 310 » » » } |
| 311 |
| 300 case sr.Failure: | 312 case sr.Failure: |
| 301 » » » build.Summary.Status = resp.Failure | 313 » » » step.Status = miloProto.Status_FAILURE |
| 314 |
| 302 default: | 315 default: |
| 303 » » » build.Summary.Status = resp.Success | 316 » » » step.Status = miloProto.Status_SUCCESS |
| 304 } | 317 } |
| 305 | 318 |
| 306 default: | 319 default: |
| 307 » » return nil, fmt.Errorf("unknown swarming task state %q", sr.Stat
e) | 320 » » return fmt.Errorf("unknown swarming task state %q", sr.State) |
| 321 » } |
| 322 |
| 323 » // Compute start and finished times. |
| 324 » if sr.StartedTs != "" { |
| 325 » » ts, err := time.Parse(SwarmingTimeLayout, sr.StartedTs) |
| 326 » » if err != nil { |
| 327 » » » return fmt.Errorf("invalid task StartedTs: %s", err) |
| 328 » » } |
| 329 » » step.Started = google.NewTimestamp(ts) |
| 330 » } |
| 331 » if sr.CompletedTs != "" { |
| 332 » » ts, err := time.Parse(SwarmingTimeLayout, sr.CompletedTs) |
| 333 » » if err != nil { |
| 334 » » » return fmt.Errorf("invalid task CompletedTs: %s", err) |
| 335 » » } |
| 336 » » step.Ended = google.NewTimestamp(ts) |
| 337 » } |
| 338 |
| 339 » return nil |
| 340 } |
| 341 |
| 342 func addTaskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsT
askResult, build *resp.MiloBuild) error { |
| 343 » build.Summary.Label = sr.TaskId |
| 344 » build.Summary.Type = resp.Recipe |
| 345 » build.Summary.Source = &resp.Link{ |
| 346 » » Label: "Task " + sr.TaskId, |
| 347 » » URL: taskPageURL(server, sr.TaskId), |
| 308 } | 348 } |
| 309 | 349 |
| 310 // Extract more swarming specific information into the properties. | 350 // Extract more swarming specific information into the properties. |
| 311 if props := taskProperties(sr); len(props.Property) > 0 { | 351 if props := taskProperties(sr); len(props.Property) > 0 { |
| 312 build.PropertyGroup = append(build.PropertyGroup, props) | 352 build.PropertyGroup = append(build.PropertyGroup, props) |
| 313 } | 353 } |
| 314 if props := tagsToProperties(sr.Tags); len(props.Property) > 0 { | 354 if props := tagsToProperties(sr.Tags); len(props.Property) > 0 { |
| 315 build.PropertyGroup = append(build.PropertyGroup, props) | 355 build.PropertyGroup = append(build.PropertyGroup, props) |
| 316 } | 356 } |
| 317 | 357 |
| 318 addBuilderLink(c, build, server, sr) | 358 addBuilderLink(c, build, server, sr) |
| 319 addBanner(build, sr) | 359 addBanner(build, sr) |
| 320 | 360 |
| 321 // Add a link to the bot. | 361 // Add a link to the bot. |
| 322 if sr.BotId != "" { | 362 if sr.BotId != "" { |
| 323 build.Summary.Bot = &resp.Link{ | 363 build.Summary.Bot = &resp.Link{ |
| 324 Label: sr.BotId, | 364 Label: sr.BotId, |
| 325 URL: botPageURL(server, sr.BotId), | 365 URL: botPageURL(server, sr.BotId), |
| 326 } | 366 } |
| 327 } | 367 } |
| 328 | 368 |
| 329 » // Compute start and finished times, and duration. | 369 » return nil |
| 330 » var err error | |
| 331 » if sr.StartedTs != "" { | |
| 332 » » build.Summary.Started, err = time.Parse(SwarmingTimeLayout, sr.S
tartedTs) | |
| 333 » » if err != nil { | |
| 334 » » » return nil, fmt.Errorf("invalid task StartedTs: %s", err
) | |
| 335 » » } | |
| 336 » } | |
| 337 » if sr.CompletedTs != "" { | |
| 338 » » build.Summary.Finished, err = time.Parse(SwarmingTimeLayout, sr.
CompletedTs) | |
| 339 » » if err != nil { | |
| 340 » » » return nil, fmt.Errorf("invalid task CompletedTs: %s", e
rr) | |
| 341 » » } | |
| 342 » } | |
| 343 » if sr.Duration != 0 { | |
| 344 » » build.Summary.Duration = time.Duration(sr.Duration * float64(tim
e.Second)) | |
| 345 » } else if sr.State == TaskRunning { | |
| 346 » » now := clock.Now(c) | |
| 347 » » if build.Summary.Started.Before(now) { | |
| 348 » » » build.Summary.Duration = now.Sub(build.Summary.Started) | |
| 349 » » } | |
| 350 » } | |
| 351 | |
| 352 » return build, nil | |
| 353 } | 370 } |
| 354 | 371 |
| 355 // streamsFromAnnotatedLog takes in an annotated log and returns a fully | 372 // streamsFromAnnotatedLog takes in an annotated log and returns a fully |
| 356 // populated set of logdog streams | 373 // populated set of logdog streams |
| 357 func streamsFromAnnotatedLog(ctx context.Context, log string) (*logdog.Streams,
error) { | 374 func streamsFromAnnotatedLog(ctx context.Context, log string) (*logdog.Streams,
error) { |
| 358 c := &memoryClient{} | 375 c := &memoryClient{} |
| 359 p := annotee.New(ctx, annotee.Options{ | 376 p := annotee.New(ctx, annotee.Options{ |
| 360 Client: c, | 377 Client: c, |
| 361 MetadataUpdateInterval: -1, // Neverrrrrr send incr updates. | 378 MetadataUpdateInterval: -1, // Neverrrrrr send incr updates. |
| 362 Offline: true, | 379 Offline: true, |
| (...skipping 25 matching lines...) Expand all Loading... |
| 388 for _, t := range sr.Tags { | 405 for _, t := range sr.Tags { |
| 389 if t == "allow_milo:1" { | 406 if t == "allow_milo:1" { |
| 390 allowMilo = true | 407 allowMilo = true |
| 391 break | 408 break |
| 392 } | 409 } |
| 393 } | 410 } |
| 394 if !allowMilo { | 411 if !allowMilo { |
| 395 return nil, fmt.Errorf("Not A Milo Job") | 412 return nil, fmt.Errorf("Not A Milo Job") |
| 396 } | 413 } |
| 397 | 414 |
| 398 » build, err := taskToBuild(c, server, sr) | 415 » var build resp.MiloBuild |
| 399 » if err != nil { | |
| 400 » » return nil, err | |
| 401 » } | |
| 402 | 416 |
| 403 // Decode the data using annotee. The logdog stream returned here is ass
umed | 417 // Decode the data using annotee. The logdog stream returned here is ass
umed |
| 404 // to be consistent, which is why the following block of code are not | 418 // to be consistent, which is why the following block of code are not |
| 405 // expected to ever err out. | 419 // expected to ever err out. |
| 406 if body != "" { | 420 if body != "" { |
| 407 lds, err := streamsFromAnnotatedLog(c, body) | 421 lds, err := streamsFromAnnotatedLog(c, body) |
| 408 if err != nil { | 422 if err != nil { |
| 409 build.Components = []*resp.BuildComponent{{ | 423 build.Components = []*resp.BuildComponent{{ |
| 410 Type: resp.Summary, | 424 Type: resp.Summary, |
| 411 Label: "Milo annotation parser", | 425 Label: "Milo annotation parser", |
| 412 Text: []string{err.Error()}, | 426 Text: []string{err.Error()}, |
| 413 Status: resp.InfraFailure, | 427 Status: resp.InfraFailure, |
| 414 SubLink: []*resp.Link{{ | 428 SubLink: []*resp.Link{{ |
| 415 Label: "swarming task", | 429 Label: "swarming task", |
| 416 URL: taskPageURL(server, taskID), | 430 URL: taskPageURL(server, taskID), |
| 417 }}, | 431 }}, |
| 418 }} | 432 }} |
| 419 } else { | 433 } else { |
| 420 » » » logdog.AddLogDogToBuild(c, linkBase, lds, build) | 434 » » » if lds.MainStream == nil || lds.MainStream.Data == nil { |
| 435 » » » » panic("no main build step stream") |
| 436 » » » } |
| 437 |
| 438 » » » if err := addTaskToMiloStep(c, server, sr, lds.MainStrea
m.Data); err != nil { |
| 439 » » » » return nil, err |
| 440 » » » } |
| 441 » » » logdog.AddLogDogToBuild(c, swarmingURLBuilder(linkBase),
lds, &build) |
| 421 } | 442 } |
| 422 } | 443 } |
| 423 | 444 |
| 424 » return build, nil | 445 » if err := addTaskToBuild(c, server, sr, &build); err != nil { |
| 446 » » return nil, err |
| 447 » } |
| 448 |
| 449 » return &build, nil |
| 425 } | 450 } |
| 426 | 451 |
| 427 // taskPageURL returns a URL to a human-consumable page of a swarming task. | 452 // taskPageURL returns a URL to a human-consumable page of a swarming task. |
| 428 // Supports server aliases. | 453 // Supports server aliases. |
| 429 func taskPageURL(swarmingHostname, taskID string) string { | 454 func taskPageURL(swarmingHostname, taskID string) string { |
| 430 return fmt.Sprintf("https://%s/user/task/%s", swarmingHostname, taskID) | 455 return fmt.Sprintf("https://%s/user/task/%s", swarmingHostname, taskID) |
| 431 } | 456 } |
| 432 | 457 |
| 433 // botPageURL returns a URL to a human-consumable page of a swarming bot. | 458 // botPageURL returns a URL to a human-consumable page of a swarming bot. |
| 434 // Supports server aliases. | 459 // Supports server aliases. |
| 435 func botPageURL(swarmingHostname, botID string) string { | 460 func botPageURL(swarmingHostname, botID string) string { |
| 436 return fmt.Sprintf("https://%s/restricted/bot/%s", swarmingHostname, bot
ID) | 461 return fmt.Sprintf("https://%s/restricted/bot/%s", swarmingHostname, bot
ID) |
| 437 } | 462 } |
| 463 |
| 464 // swarmingURLBuilder is a logdog.URLBuilder that builds Milo swarming log |
| 465 // links. |
| 466 // |
| 467 // The string value for this should be the "linkBase" parameter supplied to |
| 468 // swarmingBuildImpl. |
| 469 type swarmingURLBuilder string |
| 470 |
| 471 func (b swarmingURLBuilder) BuildLink(l *miloProto.Link) *resp.Link { |
| 472 u, err := url.Parse(string(b)) |
| 473 if err != nil { |
| 474 return nil |
| 475 } |
| 476 |
| 477 switch t := l.Value.(type) { |
| 478 case *miloProto.Link_LogdogStream: |
| 479 ls := t.LogdogStream |
| 480 |
| 481 if u.Path == "" { |
| 482 u.Path = ls.Name |
| 483 } else { |
| 484 u.Path = strings.TrimSuffix(u.Path, "/") + "/" + ls.Name |
| 485 } |
| 486 link := resp.Link{ |
| 487 Label: l.Label, |
| 488 URL: u.String(), |
| 489 } |
| 490 if link.Label == "" { |
| 491 link.Label = ls.Name |
| 492 } |
| 493 return &link |
| 494 |
| 495 default: |
| 496 return nil |
| 497 } |
| 498 } |
| OLD | NEW |