| 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 "fmt" | 9 "fmt" |
| 10 "net/http" | 10 "net/http" |
| 11 "net/url" | 11 "net/url" |
| 12 "strings" | 12 "strings" |
| 13 "time" | 13 "time" |
| 14 | 14 |
| 15 "golang.org/x/net/context" | 15 "golang.org/x/net/context" |
| 16 | 16 |
| 17 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" | 17 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" |
| 18 "github.com/luci/luci-go/common/errors" | 18 "github.com/luci/luci-go/common/errors" |
| 19 "github.com/luci/luci-go/common/logging" | 19 "github.com/luci/luci-go/common/logging" |
| 20 "github.com/luci/luci-go/common/proto/google" | 20 "github.com/luci/luci-go/common/proto/google" |
| 21 miloProto "github.com/luci/luci-go/common/proto/milo" | 21 miloProto "github.com/luci/luci-go/common/proto/milo" |
| 22 "github.com/luci/luci-go/common/sync/parallel" | 22 "github.com/luci/luci-go/common/sync/parallel" |
| 23 "github.com/luci/luci-go/logdog/client/annotee" | 23 "github.com/luci/luci-go/logdog/client/annotee" |
| 24 "github.com/luci/luci-go/logdog/client/coordinator" | 24 "github.com/luci/luci-go/logdog/client/coordinator" |
| 25 "github.com/luci/luci-go/logdog/common/types" | 25 "github.com/luci/luci-go/logdog/common/types" |
| 26 "github.com/luci/luci-go/milo/api/resp" | 26 "github.com/luci/luci-go/milo/api/resp" |
| 27 "github.com/luci/luci-go/milo/appengine/common" | 27 "github.com/luci/luci-go/milo/appengine/common" |
| 28 "github.com/luci/luci-go/milo/appengine/common/model" |
| 28 "github.com/luci/luci-go/milo/appengine/logdog" | 29 "github.com/luci/luci-go/milo/appengine/logdog" |
| 29 "github.com/luci/luci-go/server/auth" | 30 "github.com/luci/luci-go/server/auth" |
| 30 ) | 31 ) |
| 31 | 32 |
| 32 // errNotMiloJob is returned if a Swarming task is fetched that does not self- | 33 // errNotMiloJob is returned if a Swarming task is fetched that does not self- |
| 33 // identify as a Milo job. | 34 // identify as a Milo job. |
| 34 var errNotMiloJob = errors.New("Not a Milo Job or access denied") | 35 var errNotMiloJob = errors.New("Not a Milo Job or access denied") |
| 35 | 36 |
| 36 // SwarmingTimeLayout is time layout used by swarming. | 37 // SwarmingTimeLayout is time layout used by swarming. |
| 37 const SwarmingTimeLayout = "2006-01-02T15:04:05.999999999" | 38 const SwarmingTimeLayout = "2006-01-02T15:04:05.999999999" |
| (...skipping 219 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 257 } | 258 } |
| 258 } | 259 } |
| 259 return result | 260 return result |
| 260 } | 261 } |
| 261 | 262 |
| 262 // addBuilderLink adds a link to the buildbucket builder view. | 263 // addBuilderLink adds a link to the buildbucket builder view. |
| 263 func addBuilderLink(c context.Context, build *resp.MiloBuild, tags map[string]st
ring) { | 264 func addBuilderLink(c context.Context, build *resp.MiloBuild, tags map[string]st
ring) { |
| 264 bucket := tags["buildbucket_bucket"] | 265 bucket := tags["buildbucket_bucket"] |
| 265 builder := tags["builder"] | 266 builder := tags["builder"] |
| 266 if bucket != "" && builder != "" { | 267 if bucket != "" && builder != "" { |
| 267 » » build.Summary.ParentLabel = &resp.Link{ | 268 » » build.Summary.ParentLabel = resp.NewLink( |
| 268 » » » Label: builder, | 269 » » » builder, fmt.Sprintf("/buildbucket/%s/%s", bucket, build
er)) |
| 269 » » » URL: fmt.Sprintf("/buildbucket/%s/%s", bucket, builder
), | |
| 270 » » } | |
| 271 } | 270 } |
| 272 } | 271 } |
| 273 | 272 |
| 274 // addBanner adds an OS banner derived from "os" swarming tag, if present. | 273 // addBanner adds an OS banner derived from "os" swarming tag, if present. |
| 275 func addBanner(build *resp.MiloBuild, tags map[string]string) { | 274 func addBanner(build *resp.MiloBuild, tags map[string]string) { |
| 276 os := tags["os"] | 275 os := tags["os"] |
| 277 var ver string | 276 var ver string |
| 278 parts := strings.SplitN(os, "-", 2) | 277 parts := strings.SplitN(os, "-", 2) |
| 279 if len(parts) == 2 { | 278 if len(parts) == 2 { |
| 280 os = parts[0] | 279 os = parts[0] |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 401 if strings.HasPrefix(patchset, "gerrit/") { | 400 if strings.HasPrefix(patchset, "gerrit/") { |
| 402 gerritPatchset := strings.TrimLeft(patchset, "gerrit/") | 401 gerritPatchset := strings.TrimLeft(patchset, "gerrit/") |
| 403 parts := strings.Split(gerritPatchset, "/") | 402 parts := strings.Split(gerritPatchset, "/") |
| 404 if len(parts) != 3 { | 403 if len(parts) != 3 { |
| 405 // Not a well-formed gerrit patchset. | 404 // Not a well-formed gerrit patchset. |
| 406 return | 405 return |
| 407 } | 406 } |
| 408 if build.SourceStamp == nil { | 407 if build.SourceStamp == nil { |
| 409 build.SourceStamp = &resp.SourceStamp{} | 408 build.SourceStamp = &resp.SourceStamp{} |
| 410 } | 409 } |
| 411 » » build.SourceStamp.Changelist = &resp.Link{ | 410 » » build.SourceStamp.Changelist = resp.NewLink( |
| 412 » » » Label: "Gerrit CL", | 411 » » » "Gerrit CL", fmt.Sprintf("https://%s/c/%s/%s", parts[0],
parts[1], parts[2])) |
| 413 » » » URL: fmt.Sprintf("https://%s/c/%s/%s", parts[0], parts
[1], parts[2]), | |
| 414 » » } | |
| 415 | 412 |
| 416 } | 413 } |
| 417 } | 414 } |
| 418 | 415 |
| 419 func addRecipeLink(build *resp.MiloBuild, tags map[string]string) { | 416 func addRecipeLink(build *resp.MiloBuild, tags map[string]string) { |
| 420 name := tags["recipe_name"] | 417 name := tags["recipe_name"] |
| 421 repoURL := tags["recipe_repository"] | 418 repoURL := tags["recipe_repository"] |
| 422 revision := tags["recipe_revision"] | 419 revision := tags["recipe_revision"] |
| 423 if name != "" && repoURL != "" { | 420 if name != "" && repoURL != "" { |
| 424 if revision == "" { | 421 if revision == "" { |
| 425 revision = "master" | 422 revision = "master" |
| 426 } | 423 } |
| 427 // Link directly to the revision if it is a gerrit URL, otherwis
e just | 424 // Link directly to the revision if it is a gerrit URL, otherwis
e just |
| 428 // display it in the name. | 425 // display it in the name. |
| 429 if repoParse, err := url.Parse(repoURL); err == nil && strings.H
asSuffix( | 426 if repoParse, err := url.Parse(repoURL); err == nil && strings.H
asSuffix( |
| 430 repoParse.Host, ".googlesource.com") { | 427 repoParse.Host, ".googlesource.com") { |
| 431 repoURL += "/+/" + revision + "/" | 428 repoURL += "/+/" + revision + "/" |
| 432 } else { | 429 } else { |
| 433 if len(revision) > 8 { | 430 if len(revision) > 8 { |
| 434 revision = revision[:8] | 431 revision = revision[:8] |
| 435 } | 432 } |
| 436 name += " @ " + revision | 433 name += " @ " + revision |
| 437 } | 434 } |
| 438 » » build.Summary.Recipe = &resp.Link{ | 435 » » build.Summary.Recipe = resp.NewLink(name, repoURL) |
| 439 » » » Label: name, | |
| 440 » » » URL: repoURL, | |
| 441 » » } | |
| 442 } | 436 } |
| 443 } | 437 } |
| 444 | 438 |
| 445 func addTaskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsT
askResult, build *resp.MiloBuild) error { | 439 func addTaskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsT
askResult, build *resp.MiloBuild) error { |
| 446 build.Summary.Label = sr.TaskId | 440 build.Summary.Label = sr.TaskId |
| 447 build.Summary.Type = resp.Recipe | 441 build.Summary.Type = resp.Recipe |
| 448 » build.Summary.Source = &resp.Link{ | 442 » build.Summary.Source = resp.NewLink("Task "+sr.TaskId, taskPageURL(serve
r, sr.TaskId)) |
| 449 » » Label: "Task " + sr.TaskId, | |
| 450 » » URL: taskPageURL(server, sr.TaskId), | |
| 451 » } | |
| 452 | 443 |
| 453 // Extract more swarming specific information into the properties. | 444 // Extract more swarming specific information into the properties. |
| 454 if props := taskProperties(sr); len(props.Property) > 0 { | 445 if props := taskProperties(sr); len(props.Property) > 0 { |
| 455 build.PropertyGroup = append(build.PropertyGroup, props) | 446 build.PropertyGroup = append(build.PropertyGroup, props) |
| 456 } | 447 } |
| 457 tags := tagsToMap(sr.Tags) | 448 tags := tagsToMap(sr.Tags) |
| 458 | 449 |
| 459 addBuildsetInfo(build, tags) | 450 addBuildsetInfo(build, tags) |
| 460 addBanner(build, tags) | 451 addBanner(build, tags) |
| 461 addBuilderLink(c, build, tags) | 452 addBuilderLink(c, build, tags) |
| 462 addRecipeLink(build, tags) | 453 addRecipeLink(build, tags) |
| 463 | 454 |
| 464 // Add a link to the bot. | 455 // Add a link to the bot. |
| 465 if sr.BotId != "" { | 456 if sr.BotId != "" { |
| 466 » » build.Summary.Bot = &resp.Link{ | 457 » » build.Summary.Bot = resp.NewLink(sr.BotId, botPageURL(server, sr
.BotId)) |
| 467 » » » Label: sr.BotId, | |
| 468 » » » URL: botPageURL(server, sr.BotId), | |
| 469 » » } | |
| 470 } | 458 } |
| 471 | 459 |
| 472 return nil | 460 return nil |
| 473 } | 461 } |
| 474 | 462 |
| 475 // streamsFromAnnotatedLog takes in an annotated log and returns a fully | 463 // streamsFromAnnotatedLog takes in an annotated log and returns a fully |
| 476 // populated set of logdog streams | 464 // populated set of logdog streams |
| 477 func streamsFromAnnotatedLog(ctx context.Context, log string) (*logdog.Streams,
error) { | 465 func streamsFromAnnotatedLog(ctx context.Context, log string) (*logdog.Streams,
error) { |
| 478 c := &memoryClient{} | 466 c := &memoryClient{} |
| 479 p := annotee.New(ctx, annotee.Options{ | 467 p := annotee.New(ctx, annotee.Options{ |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 531 } | 519 } |
| 532 | 520 |
| 533 return &as, nil | 521 return &as, nil |
| 534 } | 522 } |
| 535 | 523 |
| 536 // failedToStart is called in the case where logdog-only mode is on but the | 524 // failedToStart is called in the case where logdog-only mode is on but the |
| 537 // stream doesn't exist and the swarming job is complete. It modifies the build | 525 // stream doesn't exist and the swarming job is complete. It modifies the build |
| 538 // to add information that would've otherwise been in the annotation stream. | 526 // to add information that would've otherwise been in the annotation stream. |
| 539 func failedToStart(c context.Context, build *resp.MiloBuild, res *swarming.Swarm
ingRpcsTaskResult, host string) error { | 527 func failedToStart(c context.Context, build *resp.MiloBuild, res *swarming.Swarm
ingRpcsTaskResult, host string) error { |
| 540 var err error | 528 var err error |
| 541 » build.Summary.Status = resp.InfraFailure | 529 » build.Summary.Status = model.InfraFailure |
| 542 build.Summary.Started, err = time.Parse(SwarmingTimeLayout, res.StartedT
s) | 530 build.Summary.Started, err = time.Parse(SwarmingTimeLayout, res.StartedT
s) |
| 543 if err != nil { | 531 if err != nil { |
| 544 return err | 532 return err |
| 545 } | 533 } |
| 546 build.Summary.Finished, err = time.Parse(SwarmingTimeLayout, res.Complet
edTs) | 534 build.Summary.Finished, err = time.Parse(SwarmingTimeLayout, res.Complet
edTs) |
| 547 if err != nil { | 535 if err != nil { |
| 548 return err | 536 return err |
| 549 } | 537 } |
| 550 build.Summary.Duration = build.Summary.Finished.Sub(build.Summary.Starte
d) | 538 build.Summary.Duration = build.Summary.Finished.Sub(build.Summary.Starte
d) |
| 551 » infoComp := infoComponent(resp.InfraFailure, | 539 » infoComp := infoComponent(model.InfraFailure, |
| 552 "LogDog stream not found", "Job likely failed to start.") | 540 "LogDog stream not found", "Job likely failed to start.") |
| 553 infoComp.Started = build.Summary.Started | 541 infoComp.Started = build.Summary.Started |
| 554 infoComp.Finished = build.Summary.Finished | 542 infoComp.Finished = build.Summary.Finished |
| 555 infoComp.Duration = build.Summary.Duration | 543 infoComp.Duration = build.Summary.Duration |
| 556 infoComp.Verbosity = resp.Interesting | 544 infoComp.Verbosity = resp.Interesting |
| 557 build.Components = append(build.Components, infoComp) | 545 build.Components = append(build.Components, infoComp) |
| 558 return addTaskToBuild(c, host, res, build) | 546 return addTaskToBuild(c, host, res, build) |
| 559 } | 547 } |
| 560 | 548 |
| 561 func (bl *buildLoader) swarmingBuildImpl(c context.Context, svc swarmingService,
linkBase, taskID string) (*resp.MiloBuild, error) { | 549 func (bl *buildLoader) swarmingBuildImpl(c context.Context, svc swarmingService,
linkBase, taskID string) (*resp.MiloBuild, error) { |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 630 // The stream was not found. This could be due
to one of two things: | 618 // The stream was not found. This could be due
to one of two things: |
| 631 // 1. The step just started and we're just waiti
ng for the logs | 619 // 1. The step just started and we're just waiti
ng for the logs |
| 632 // to propogage to logdog. | 620 // to propogage to logdog. |
| 633 // 2. The bootsrap on the client failed, and nev
er sent data to logdog. | 621 // 2. The bootsrap on the client failed, and nev
er sent data to logdog. |
| 634 // This would be evident because the swarming re
sult would be a failure. | 622 // This would be evident because the swarming re
sult would be a failure. |
| 635 if fr.res.State == TaskCompleted { | 623 if fr.res.State == TaskCompleted { |
| 636 err = failedToStart(c, &build, fr.res, s
vc.getHost()) | 624 err = failedToStart(c, &build, fr.res, s
vc.getHost()) |
| 637 return &build, err | 625 return &build, err |
| 638 } | 626 } |
| 639 logging.WithError(err).Errorf(c, "User cannot ac
cess stream.") | 627 logging.WithError(err).Errorf(c, "User cannot ac
cess stream.") |
| 640 » » » » build.Components = append(build.Components, info
Component(resp.Running, | 628 » » » » build.Components = append(build.Components, info
Component(model.Running, |
| 641 "Waiting...", "waiting for annotation st
ream")) | 629 "Waiting...", "waiting for annotation st
ream")) |
| 642 | 630 |
| 643 case coordinator.ErrNoAccess: | 631 case coordinator.ErrNoAccess: |
| 644 logging.WithError(err).Errorf(c, "User cannot ac
cess stream.") | 632 logging.WithError(err).Errorf(c, "User cannot ac
cess stream.") |
| 645 » » » » build.Components = append(build.Components, info
Component(resp.Failure, | 633 » » » » build.Components = append(build.Components, info
Component(model.Failure, |
| 646 "No Access", "no access to annotation st
ream")) | 634 "No Access", "no access to annotation st
ream")) |
| 647 | 635 |
| 648 default: | 636 default: |
| 649 logging.WithError(err).Errorf(c, "Failed to load
LogDog annotation stream.") | 637 logging.WithError(err).Errorf(c, "Failed to load
LogDog annotation stream.") |
| 650 » » » » build.Components = append(build.Components, info
Component(resp.InfraFailure, | 638 » » » » build.Components = append(build.Components, info
Component(model.InfraFailure, |
| 651 "Error", "failed to load annotation stre
am")) | 639 "Error", "failed to load annotation stre
am")) |
| 652 } | 640 } |
| 653 } | 641 } |
| 654 | 642 |
| 655 case fr.log != "": | 643 case fr.log != "": |
| 656 // Decode the data using annotee. The logdog stream returned her
e is assumed | 644 // Decode the data using annotee. The logdog stream returned her
e is assumed |
| 657 // to be consistent, which is why the following block of code ar
e not | 645 // to be consistent, which is why the following block of code ar
e not |
| 658 // expected to ever err out. | 646 // expected to ever err out. |
| 659 var err error | 647 var err error |
| 660 lds, err = streamsFromAnnotatedLog(c, fr.log) | 648 lds, err = streamsFromAnnotatedLog(c, fr.log) |
| 661 if err != nil { | 649 if err != nil { |
| 662 » » » comp := infoComponent(resp.InfraFailure, "Milo annotatio
n parser", err.Error()) | 650 » » » comp := infoComponent(model.InfraFailure, "Milo annotati
on parser", err.Error()) |
| 663 » » » comp.SubLink = append(comp.SubLink, resp.LinkSet{&resp.L
ink{ | 651 » » » comp.SubLink = append(comp.SubLink, resp.LinkSet{ |
| 664 » » » » Label: "swarming task", | 652 » » » » resp.NewLink("swarming task", taskPageURL(svc.ge
tHost(), taskID)), |
| 665 » » » » URL: taskPageURL(svc.getHost(), taskID), | 653 » » » }) |
| 666 » » » }}) | |
| 667 build.Components = append(build.Components, comp) | 654 build.Components = append(build.Components, comp) |
| 668 } | 655 } |
| 669 | 656 |
| 670 if lds != nil && lds.MainStream != nil && lds.MainStream.Data !=
nil { | 657 if lds != nil && lds.MainStream != nil && lds.MainStream.Data !=
nil { |
| 671 s = lds.MainStream.Data | 658 s = lds.MainStream.Data |
| 672 } | 659 } |
| 673 ub = swarmingURLBuilder(linkBase) | 660 ub = swarmingURLBuilder(linkBase) |
| 674 | 661 |
| 675 default: | 662 default: |
| 676 s = &miloProto.Step{} | 663 s = &miloProto.Step{} |
| 677 ub = swarmingURLBuilder(linkBase) | 664 ub = swarmingURLBuilder(linkBase) |
| 678 } | 665 } |
| 679 | 666 |
| 680 if s != nil { | 667 if s != nil { |
| 681 if err := addTaskToMiloStep(c, svc.getHost(), fr.res, s); err !=
nil { | 668 if err := addTaskToMiloStep(c, svc.getHost(), fr.res, s); err !=
nil { |
| 682 return nil, err | 669 return nil, err |
| 683 } | 670 } |
| 684 logdog.AddLogDogToBuild(c, ub, s, &build) | 671 logdog.AddLogDogToBuild(c, ub, s, &build) |
| 685 } | 672 } |
| 686 | 673 |
| 687 if err := addTaskToBuild(c, svc.getHost(), fr.res, &build); err != nil { | 674 if err := addTaskToBuild(c, svc.getHost(), fr.res, &build); err != nil { |
| 688 return nil, err | 675 return nil, err |
| 689 } | 676 } |
| 690 | 677 |
| 691 return &build, nil | 678 return &build, nil |
| 692 } | 679 } |
| 693 | 680 |
| 694 func infoComponent(st resp.Status, label, text string) *resp.BuildComponent { | 681 func infoComponent(st model.Status, label, text string) *resp.BuildComponent { |
| 695 return &resp.BuildComponent{ | 682 return &resp.BuildComponent{ |
| 696 Type: resp.Summary, | 683 Type: resp.Summary, |
| 697 Label: label, | 684 Label: label, |
| 698 Text: []string{text}, | 685 Text: []string{text}, |
| 699 Status: st, | 686 Status: st, |
| 700 } | 687 } |
| 701 } | 688 } |
| 702 | 689 |
| 703 // isAllowed checks if: | 690 // isAllowed checks if: |
| 704 // 1. allow_milo:1 is present. If so, it's a public job. | 691 // 1. allow_milo:1 is present. If so, it's a public job. |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 755 | 742 |
| 756 switch t := l.Value.(type) { | 743 switch t := l.Value.(type) { |
| 757 case *miloProto.Link_LogdogStream: | 744 case *miloProto.Link_LogdogStream: |
| 758 ls := t.LogdogStream | 745 ls := t.LogdogStream |
| 759 | 746 |
| 760 if u.Path == "" { | 747 if u.Path == "" { |
| 761 u.Path = ls.Name | 748 u.Path = ls.Name |
| 762 } else { | 749 } else { |
| 763 u.Path = strings.TrimSuffix(u.Path, "/") + "/" + ls.Name | 750 u.Path = strings.TrimSuffix(u.Path, "/") + "/" + ls.Name |
| 764 } | 751 } |
| 765 » » link := resp.Link{ | 752 » » link := resp.NewLink(l.Label, u.String()) |
| 766 » » » Label: l.Label, | |
| 767 » » » URL: u.String(), | |
| 768 » » } | |
| 769 if link.Label == "" { | 753 if link.Label == "" { |
| 770 link.Label = ls.Name | 754 link.Label = ls.Name |
| 771 } | 755 } |
| 772 » » return &link | 756 » » return link |
| 773 | 757 |
| 774 case *miloProto.Link_Url: | 758 case *miloProto.Link_Url: |
| 775 » » return &resp.Link{ | 759 » » return resp.NewLink(l.Label, t.Url) |
| 776 » » » Label: l.Label, | |
| 777 » » » URL: t.Url, | |
| 778 » » } | |
| 779 | 760 |
| 780 default: | 761 default: |
| 781 return nil | 762 return nil |
| 782 } | 763 } |
| 783 } | 764 } |
| 784 | 765 |
| 785 func swarmingTags(v []string) map[string]string { | 766 func swarmingTags(v []string) map[string]string { |
| 786 res := make(map[string]string, len(v)) | 767 res := make(map[string]string, len(v)) |
| 787 for _, tag := range v { | 768 for _, tag := range v { |
| 788 var value string | 769 var value string |
| 789 parts := strings.SplitN(tag, ":", 2) | 770 parts := strings.SplitN(tag, ":", 2) |
| 790 if len(parts) == 2 { | 771 if len(parts) == 2 { |
| 791 value = parts[1] | 772 value = parts[1] |
| 792 } | 773 } |
| 793 res[parts[0]] = value | 774 res[parts[0]] = value |
| 794 } | 775 } |
| 795 return res | 776 return res |
| 796 } | 777 } |
| OLD | NEW |