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