| OLD | NEW |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. | 1 // Copyright 2017 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 "strconv" | 8 "strconv" |
| 9 "strings" | 9 "strings" |
| 10 "unicode/utf8" | 10 "unicode/utf8" |
| 11 | 11 |
| 12 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" | 12 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" |
| 13 "github.com/luci/luci-go/common/errors" | 13 "github.com/luci/luci-go/common/errors" |
| 14 "github.com/luci/luci-go/common/logging" | 14 "github.com/luci/luci-go/common/logging" |
| 15 miloProto "github.com/luci/luci-go/common/proto/milo" | 15 miloProto "github.com/luci/luci-go/common/proto/milo" |
| 16 "github.com/luci/luci-go/grpc/grpcutil" | 16 "github.com/luci/luci-go/grpc/grpcutil" |
| 17 "github.com/luci/luci-go/logdog/client/coordinator" | |
| 18 "github.com/luci/luci-go/logdog/common/types" | 17 "github.com/luci/luci-go/logdog/common/types" |
| 19 "github.com/luci/luci-go/luci_config/common/cfgtypes" | 18 "github.com/luci/luci-go/luci_config/common/cfgtypes" |
| 20 milo "github.com/luci/luci-go/milo/api/proto" | 19 milo "github.com/luci/luci-go/milo/api/proto" |
| 21 "github.com/luci/luci-go/milo/appengine/logdog" | |
| 22 | 20 |
| 23 "golang.org/x/net/context" | 21 "golang.org/x/net/context" |
| 24 ) | 22 ) |
| 25 | 23 |
| 26 // BuildInfoProvider provides build information. | 24 // BuildInfoProvider provides build information. |
| 27 // | 25 // |
| 28 // In a production system, this will be completely defaults. For testing, the | 26 // In a production system, this will be completely defaults. For testing, the |
| 29 // various services and data sources may be substituted for testing stubs. | 27 // various services and data sources may be substituted for testing stubs. |
| 30 type BuildInfoProvider struct { | 28 type BuildInfoProvider struct { |
| 31 » // LogdogClientFunc returns a coordinator Client instance for the suppli
ed | 29 » bl buildLoader |
| 32 » // parameters. | |
| 33 » // | |
| 34 » // If nil, a production client will be generated. | |
| 35 » LogdogClientFunc func(c context.Context) (*coordinator.Client, error) | |
| 36 | 30 |
| 37 // swarmingServiceFunc returns a swarmingService instance for the suppli
ed | 31 // swarmingServiceFunc returns a swarmingService instance for the suppli
ed |
| 38 // parameters. | 32 // parameters. |
| 39 // | 33 // |
| 40 // If nil, a production fetcher will be generated. | 34 // If nil, a production fetcher will be generated. |
| 41 swarmingServiceFunc func(c context.Context, host string) (swarmingServic
e, error) | 35 swarmingServiceFunc func(c context.Context, host string) (swarmingServic
e, error) |
| 42 } | 36 } |
| 43 | 37 |
| 44 func (p *BuildInfoProvider) newLogdogClient(c context.Context) (*coordinator.Cli
ent, error) { | |
| 45 if p.LogdogClientFunc != nil { | |
| 46 return p.LogdogClientFunc(c) | |
| 47 } | |
| 48 return logdog.NewClient(c, "") | |
| 49 } | |
| 50 | |
| 51 func (p *BuildInfoProvider) newSwarmingService(c context.Context, host string) (
swarmingService, error) { | 38 func (p *BuildInfoProvider) newSwarmingService(c context.Context, host string) (
swarmingService, error) { |
| 52 fn := p.swarmingServiceFunc | 39 fn := p.swarmingServiceFunc |
| 53 if fn == nil { | 40 if fn == nil { |
| 54 fn = getSwarmingService | 41 fn = getSwarmingService |
| 55 } | 42 } |
| 56 return fn(c, host) | 43 return fn(c, host) |
| 57 } | 44 } |
| 58 | 45 |
| 59 // GetBuildInfo resolves a Milo protobuf Step for a given Swarming task. | 46 // GetBuildInfo resolves a Milo protobuf Step for a given Swarming task. |
| 60 func (p *BuildInfoProvider) GetBuildInfo(c context.Context, req *milo.BuildInfoR
equest_Swarming, | 47 func (p *BuildInfoProvider) GetBuildInfo(c context.Context, req *milo.BuildInfoR
equest_Swarming, |
| (...skipping 21 matching lines...) Expand all Loading... |
| 82 return nil, grpcutil.NotFound | 69 return nil, grpcutil.NotFound |
| 83 } | 70 } |
| 84 | 71 |
| 85 logging.WithError(err).Errorf(c, "Failed to load Swarming task."
) | 72 logging.WithError(err).Errorf(c, "Failed to load Swarming task."
) |
| 86 return nil, grpcutil.Internal | 73 return nil, grpcutil.Internal |
| 87 } | 74 } |
| 88 | 75 |
| 89 // Determine the LogDog annotation stream path for this Swarming task. | 76 // Determine the LogDog annotation stream path for this Swarming task. |
| 90 // | 77 // |
| 91 // On failure, will return a gRPC error. | 78 // On failure, will return a gRPC error. |
| 92 » as, err := resolveLogDogAnnotations(c, fr.req, projectHint, host, req.Ta
sk, fr.res.TryNumber) | 79 » stream, err := resolveLogDogAnnotations(c, fr.req, projectHint, host, re
q.Task, fr.res.TryNumber) |
| 93 if err != nil { | 80 if err != nil { |
| 94 logging.WithError(err).Warningf(c, "Failed to get annotation str
eam parameters.") | 81 logging.WithError(err).Warningf(c, "Failed to get annotation str
eam parameters.") |
| 95 return nil, err | 82 return nil, err |
| 96 } | 83 } |
| 97 if err := as.Normalize(); err != nil { | |
| 98 logging.WithError(err).Warningf(c, "Failed to normalize annotati
on stream parameters.") | |
| 99 return nil, grpcutil.Internal | |
| 100 } | |
| 101 | 84 |
| 102 logging.Fields{ | 85 logging.Fields{ |
| 103 » » "project": as.Project, | 86 » » "host": stream.Host, |
| 104 » » "path": as.Path, | 87 » » "project": stream.Project, |
| 105 » }.Infof(c, "Resolved annotation stream.") | 88 » » "path": stream.Path, |
| 89 » }.Infof(c, "Resolved LogDog annotation stream.") |
| 106 | 90 |
| 107 » client, err := p.newLogdogClient(c) | 91 » as, err := p.bl.newEmptyAnnotationStream(c, stream) |
| 108 if err != nil { | 92 if err != nil { |
| 109 » » logging.WithError(err).Errorf(c, "Failed to create LogDog client
.") | 93 » » logging.WithError(err).Errorf(c, "Failed to create LogDog annota
tion stream.") |
| 110 return nil, grpcutil.Internal | 94 return nil, grpcutil.Internal |
| 111 } | 95 } |
| 112 | 96 |
| 113 // Fetch LogDog annotation stream data. | 97 // Fetch LogDog annotation stream data. |
| 114 » as.Client = client | 98 » step, err := as.Fetch(c) |
| 115 » step, err := as.Load(c) | |
| 116 if err != nil { | 99 if err != nil { |
| 117 logging.WithError(err).Warningf(c, "Failed to load annotation st
ream.") | 100 logging.WithError(err).Warningf(c, "Failed to load annotation st
ream.") |
| 118 return nil, grpcutil.Internal | 101 return nil, grpcutil.Internal |
| 119 } | 102 } |
| 120 | 103 |
| 121 // Add Swarming task parameters to the Milo step. | 104 // Add Swarming task parameters to the Milo step. |
| 122 if err := addTaskToMiloStep(c, sf.getHost(), fr.res, step); err != nil { | 105 if err := addTaskToMiloStep(c, sf.getHost(), fr.res, step); err != nil { |
| 123 return nil, err | 106 return nil, err |
| 124 } | 107 } |
| 125 | 108 |
| 126 prefix, name := as.Path.Split() | 109 prefix, name := as.Path.Split() |
| 127 return &milo.BuildInfoResponse{ | 110 return &milo.BuildInfoResponse{ |
| 128 Project: string(as.Project), | 111 Project: string(as.Project), |
| 129 Step: step, | 112 Step: step, |
| 130 AnnotationStream: &miloProto.LogdogStream{ | 113 AnnotationStream: &miloProto.LogdogStream{ |
| 131 » » » Server: client.Host, | 114 » » » Server: as.Client.Host, |
| 132 Prefix: string(prefix), | 115 Prefix: string(prefix), |
| 133 Name: string(name), | 116 Name: string(name), |
| 134 }, | 117 }, |
| 135 }, nil | 118 }, nil |
| 136 } | 119 } |
| 137 | 120 |
| 138 // resolveLogDogAnnotations returns a configured AnnotationStream given the inpu
t | 121 // resolveLogDogAnnotations returns a configured AnnotationStream given the inpu
t |
| 139 // parameters. | 122 // parameters. |
| 140 // | 123 // |
| 141 // This will return a gRPC error on failure. | 124 // This will return a gRPC error on failure. |
| 142 // | 125 // |
| 143 // This function is messy and implementation-specific. That's the point of this | 126 // This function is messy and implementation-specific. That's the point of this |
| 144 // endpoint, though. All of the nastiness here should be replaced with something | 127 // endpoint, though. All of the nastiness here should be replaced with something |
| 145 // more elegant once that becomes available. In the meantime... | 128 // more elegant once that becomes available. In the meantime... |
| 146 func resolveLogDogAnnotations(c context.Context, sr *swarming.SwarmingRpcsTaskRe
quest, projectHint cfgtypes.ProjectName, | 129 func resolveLogDogAnnotations(c context.Context, sr *swarming.SwarmingRpcsTaskRe
quest, projectHint cfgtypes.ProjectName, |
| 147 » host, taskID string, tryNumber int64) (*logdog.AnnotationStream, error)
{ | 130 » host, taskID string, tryNumber int64) (*types.StreamAddr, error) { |
| 131 |
| 132 » // Try and resolve from explicit tags (preferred). |
| 133 » tags := swarmingTags(sr.Tags) |
| 134 » addr, err := resolveLogDogStreamAddrFromTags(tags) |
| 135 » if err == nil { |
| 136 » » return addr, nil |
| 137 » } |
| 138 » logging.WithError(err).Debugf(c, "Could not resolve stream address from
tags.") |
| 148 | 139 |
| 149 // If this is a Kitchen command, maybe we can infer our LogDog project f
rom | 140 // If this is a Kitchen command, maybe we can infer our LogDog project f
rom |
| 150 // the command-line. | 141 // the command-line. |
| 151 var as logdog.AnnotationStream | |
| 152 if sr.Properties == nil { | 142 if sr.Properties == nil { |
| 153 logging.Warningf(c, "No request properties, can't infer annotati
on stream path.") | 143 logging.Warningf(c, "No request properties, can't infer annotati
on stream path.") |
| 154 return nil, grpcutil.NotFound | 144 return nil, grpcutil.NotFound |
| 155 } | 145 } |
| 156 | 146 |
| 147 addr = &types.StreamAddr{} |
| 157 var isKitchen bool | 148 var isKitchen bool |
| 158 » if as.Project, isKitchen = getLogDogProjectFromKitchen(sr.Properties.Com
mand); !isKitchen { | 149 » if addr.Project, isKitchen = getLogDogProjectFromKitchen(sr.Properties.C
ommand); !isKitchen { |
| 159 logging.Warningf(c, "Not a Kitchen CLI, can't infer annotation s
tream path.") | 150 logging.Warningf(c, "Not a Kitchen CLI, can't infer annotation s
tream path.") |
| 160 return nil, grpcutil.NotFound | 151 return nil, grpcutil.NotFound |
| 161 } | 152 } |
| 162 | 153 |
| 163 » if as.Project == "" { | 154 » if addr.Project == "" { |
| 164 » » as.Project = projectHint | 155 » » addr.Project = projectHint |
| 165 } | 156 } |
| 166 » if as.Project == "" { | 157 » if addr.Project == "" { |
| 167 logging.Warningf(c, "Don't know how to get annotation stream pat
h.") | 158 logging.Warningf(c, "Don't know how to get annotation stream pat
h.") |
| 168 return nil, grpcutil.NotFound | 159 return nil, grpcutil.NotFound |
| 169 } | 160 } |
| 170 | 161 |
| 171 // This is a Kitchen run, and it has a project! Construct the annotation | 162 // This is a Kitchen run, and it has a project! Construct the annotation |
| 172 // stream path. | 163 // stream path. |
| 173 // | 164 // |
| 174 // This is an implementation of: | 165 // This is an implementation of: |
| 175 // https://chromium.googlesource.com/infra/infra/+/a7032e3e240d4b81a1912
bfaf29a20d02f665cc1/go/src/infra/tools/kitchen/cook_logdog.go#129 | 166 // https://chromium.googlesource.com/infra/infra/+/a7032e3e240d4b81a1912
bfaf29a20d02f665cc1/go/src/infra/tools/kitchen/cook_logdog.go#129 |
| 176 runID, err := getRunID(taskID, tryNumber) | 167 runID, err := getRunID(taskID, tryNumber) |
| 177 if err != nil { | 168 if err != nil { |
| 178 logging.Fields{ | 169 logging.Fields{ |
| 179 logging.ErrorKey: err, | 170 logging.ErrorKey: err, |
| 180 "taskID": taskID, | 171 "taskID": taskID, |
| 181 "tryNumber": tryNumber, | 172 "tryNumber": tryNumber, |
| 182 }.Errorf(c, "Failed to get Run ID for task/try.") | 173 }.Errorf(c, "Failed to get Run ID for task/try.") |
| 183 return nil, grpcutil.Internal | 174 return nil, grpcutil.Internal |
| 184 } | 175 } |
| 185 | 176 |
| 186 prefix, err := types.MakeStreamName("", "swarm", host, runID) | 177 prefix, err := types.MakeStreamName("", "swarm", host, runID) |
| 187 if err != nil { | 178 if err != nil { |
| 188 logging.WithError(err).Errorf(c, "Failed to generate Swarming pr
efix.") | 179 logging.WithError(err).Errorf(c, "Failed to generate Swarming pr
efix.") |
| 189 return nil, grpcutil.Internal | 180 return nil, grpcutil.Internal |
| 190 } | 181 } |
| 191 » as.Path = prefix.Join("annotations") | 182 » addr.Path = prefix.Join("annotations") |
| 192 » return &as, nil | 183 » return addr, nil |
| 193 } | 184 } |
| 194 | 185 |
| 195 func getLogDogProjectFromKitchen(cmd []string) (proj cfgtypes.ProjectName, isKit
chen bool) { | 186 func getLogDogProjectFromKitchen(cmd []string) (proj cfgtypes.ProjectName, isKit
chen bool) { |
| 196 // Is this a Kitchen command? | 187 // Is this a Kitchen command? |
| 197 switch { | 188 switch { |
| 198 case len(cmd) == 0: | 189 case len(cmd) == 0: |
| 199 return | 190 return |
| 200 case !strings.HasPrefix(cmd[0], "kitchen"): | 191 case !strings.HasPrefix(cmd[0], "kitchen"): |
| 201 return | 192 return |
| 202 } | 193 } |
| 203 isKitchen = true | 194 isKitchen = true |
| 204 cmd = cmd[1:] | 195 cmd = cmd[1:] |
| 205 | 196 |
| 206 // Scan through for the "-logdog-project" argument. | 197 // Scan through for the "-logdog-project" argument. |
| 207 for i, arg := range cmd { | 198 for i, arg := range cmd { |
| 208 if arg == "-logdog-project" { | 199 if arg == "-logdog-project" { |
| 209 if i < len(cmd)-2 { | 200 if i < len(cmd)-2 { |
| 210 proj = cfgtypes.ProjectName(cmd[i+1]) | 201 proj = cfgtypes.ProjectName(cmd[i+1]) |
| 211 return | 202 return |
| 212 } | 203 } |
| 213 break | 204 break |
| 214 } | 205 } |
| 215 } | 206 } |
| 216 return | 207 return |
| 217 } | 208 } |
| 218 | 209 |
| 210 func resolveLogDogStreamAddrFromTags(tags map[string]string) (*types.StreamAddr,
error) { |
| 211 // If we don't have a LUCI project, abort. |
| 212 luciProject, logLocation := tags["luci_project"], tags["log_location"] |
| 213 switch { |
| 214 case luciProject == "": |
| 215 return nil, errors.New("no 'luci_project' tag") |
| 216 case logLocation == "": |
| 217 return nil, errors.New("no 'log_location' tag") |
| 218 } |
| 219 |
| 220 addr, err := types.ParseURL(logLocation) |
| 221 if err != nil { |
| 222 return nil, errors.Annotate(err).Reason("could not parse LogDog
stream from location").Err() |
| 223 } |
| 224 |
| 225 // The LogDog stream's project should match the LUCI project. |
| 226 if string(addr.Project) != luciProject { |
| 227 return nil, errors.Reason("stream project %(streamProject)q does
n't match LUCI project %(luciProject)q"). |
| 228 D("luciProject", luciProject). |
| 229 D("streamProject", addr.Project). |
| 230 Err() |
| 231 } |
| 232 |
| 233 return addr, nil |
| 234 } |
| 235 |
| 219 // getRunID converts a Swarming task ID and try number into a Swarming Run ID. | 236 // getRunID converts a Swarming task ID and try number into a Swarming Run ID. |
| 220 // | 237 // |
| 221 // The run ID is a permutation of the last four bits of the Swarming Task ID. | 238 // The run ID is a permutation of the last four bits of the Swarming Task ID. |
| 222 // Therefore, we chop it off of the string, mutate it, and then add it back. | 239 // Therefore, we chop it off of the string, mutate it, and then add it back. |
| 223 // | 240 // |
| 224 // TODO(dnj): Replace this with Swarming API call once finished. | 241 // TODO(dnj): Replace this with Swarming API call once finished. |
| 225 func getRunID(taskID string, tryNumber int64) (string, error) { | 242 func getRunID(taskID string, tryNumber int64) (string, error) { |
| 226 // Slice off the last character form the task ID. | 243 // Slice off the last character form the task ID. |
| 227 if len(taskID) == 0 { | 244 if len(taskID) == 0 { |
| 228 return "", errors.New("swarming task ID is empty") | 245 return "", errors.New("swarming task ID is empty") |
| 229 } | 246 } |
| 230 | 247 |
| 231 // Parse "tryNumber" as a hex string. | 248 // Parse "tryNumber" as a hex string. |
| 232 if tryNumber < 0 || tryNumber > 0x0F { | 249 if tryNumber < 0 || tryNumber > 0x0F { |
| 233 return "", errors.Reason("try number %(try)d exceeds 4 bits"). | 250 return "", errors.Reason("try number %(try)d exceeds 4 bits"). |
| 234 D("try", tryNumber). | 251 D("try", tryNumber). |
| 235 Err() | 252 Err() |
| 236 } | 253 } |
| 237 | 254 |
| 238 lastChar, lastCharSize := utf8.DecodeLastRuneInString(taskID) | 255 lastChar, lastCharSize := utf8.DecodeLastRuneInString(taskID) |
| 239 v, err := strconv.ParseUint(string(lastChar), 16, 8) | 256 v, err := strconv.ParseUint(string(lastChar), 16, 8) |
| 240 if err != nil { | 257 if err != nil { |
| 241 return "", errors.Annotate(err).Reason("failed to parse hex from
rune: %(rune)r"). | 258 return "", errors.Annotate(err).Reason("failed to parse hex from
rune: %(rune)r"). |
| 242 D("rune", lastChar). | 259 D("rune", lastChar). |
| 243 Err() | 260 Err() |
| 244 } | 261 } |
| 245 | 262 |
| 246 return taskID[:len(taskID)-lastCharSize] + strconv.FormatUint((v|uint64(
tryNumber)), 16), nil | 263 return taskID[:len(taskID)-lastCharSize] + strconv.FormatUint((v|uint64(
tryNumber)), 16), nil |
| 247 } | 264 } |
| OLD | NEW |