| 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 |