Chromium Code Reviews| 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 "path/filepath" | 12 "path/filepath" |
| 13 "strings" | 13 "strings" |
| 14 "sync" | 14 "sync" |
| 15 "time" | 15 "time" |
| 16 | 16 |
| 17 "golang.org/x/net/context" | 17 "golang.org/x/net/context" |
| 18 | 18 |
| 19 "github.com/luci/luci-go/appengine/cmd/milo/logdog" | 19 "github.com/luci/luci-go/appengine/cmd/milo/logdog" |
| 20 "github.com/luci/luci-go/appengine/cmd/milo/resp" | 20 "github.com/luci/luci-go/appengine/cmd/milo/resp" |
| 21 "github.com/luci/luci-go/appengine/gaeauth/client" | 21 "github.com/luci/luci-go/appengine/gaeauth/client" |
| 22 "github.com/luci/luci-go/client/logdog/annotee" | 22 "github.com/luci/luci-go/client/logdog/annotee" |
| 23 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" | 23 swarming "github.com/luci/luci-go/common/api/swarming/swarming/v1" |
| 24 "github.com/luci/luci-go/common/clock" | 24 "github.com/luci/luci-go/common/clock" |
| 25 "github.com/luci/luci-go/common/logdog/types" | 25 "github.com/luci/luci-go/common/logdog/types" |
| 26 "github.com/luci/luci-go/common/logging" | |
| 27 miloProto "github.com/luci/luci-go/common/proto/milo" | |
| 28 "github.com/luci/luci-go/common/transport" | 26 "github.com/luci/luci-go/common/transport" |
| 29 ) | 27 ) |
| 30 | 28 |
| 31 // Swarming task states.. | 29 // Swarming task states.. |
| 32 const ( | 30 const ( |
| 33 // TaskRunning means task is running. | 31 // TaskRunning means task is running. |
| 34 TaskRunning = "RUNNING" | 32 TaskRunning = "RUNNING" |
| 35 // TaskPending means task didn't start yet. | 33 // TaskPending means task didn't start yet. |
| 36 TaskPending = "PENDING" | 34 TaskPending = "PENDING" |
| 37 // TaskExpired means task expired and did not start. | 35 // TaskExpired means task expired and did not start. |
| 38 TaskExpired = "EXPIRED" | 36 TaskExpired = "EXPIRED" |
| 39 // TaskTimedOut means task started, but took too long. | 37 // TaskTimedOut means task started, but took too long. |
| 40 TaskTimedOut = "TIMED_OUT" | 38 TaskTimedOut = "TIMED_OUT" |
| 41 // TaskBotDied means task started but bot died. | 39 // TaskBotDied means task started but bot died. |
| 42 TaskBotDied = "BOT_DIED" | 40 TaskBotDied = "BOT_DIED" |
| 43 // TaskCanceled means the task was canceled. See CompletedTs to determin e whether it was started. | 41 // TaskCanceled means the task was canceled. See CompletedTs to determin e whether it was started. |
| 44 TaskCanceled = "CANCELED" | 42 TaskCanceled = "CANCELED" |
| 45 // TaskCompleted means task is complete. | 43 // TaskCompleted means task is complete. |
| 46 TaskCompleted = "COMPLETED" | 44 TaskCompleted = "COMPLETED" |
| 47 ) | 45 ) |
| 48 | 46 |
| 49 func resolveServer(server string) string { | 47 func resolveServer(server string) string { |
| 50 // TODO(hinoka): configure this map in luci-config | 48 // TODO(hinoka): configure this map in luci-config |
| 51 » if server == "" || server == "default" || server == "dev" { | 49 » switch server { |
| 50 » case "", "default", "dev": | |
| 52 return "chromium-swarm-dev.appspot.com" | 51 return "chromium-swarm-dev.appspot.com" |
| 53 » } else if server == "prod" { | 52 |
| 53 » case "prod": | |
| 54 return "chromium-swarm.appspot.com" | 54 return "chromium-swarm.appspot.com" |
| 55 » } else { | 55 |
| 56 » default: | |
| 56 return server | 57 return server |
| 57 } | 58 } |
| 58 } | 59 } |
| 59 | 60 |
| 60 func getSwarmingClient(c context.Context, server string) (*swarming.Service, err or) { | 61 func getSwarmingClient(c context.Context, server string) (*swarming.Service, err or) { |
| 61 c, _ = context.WithTimeout(c, 60*time.Second) | 62 c, _ = context.WithTimeout(c, 60*time.Second) |
| 62 client := transport.GetClient(client.UseServiceAccountTransport( | 63 client := transport.GetClient(client.UseServiceAccountTransport( |
| 63 c, []string{"https://www.googleapis.com/auth/userinfo.email"}, n il)) | 64 c, []string{"https://www.googleapis.com/auth/userinfo.email"}, n il)) |
| 64 sc, err := swarming.New(client) | 65 sc, err := swarming.New(client) |
| 65 if err != nil { | 66 if err != nil { |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 146 defer wg.Done() | 147 defer wg.Done() |
| 147 sr, errRes = getSwarmingResult(sc, taskID) | 148 sr, errRes = getSwarmingResult(sc, taskID) |
| 148 }() | 149 }() |
| 149 wg.Wait() | 150 wg.Wait() |
| 150 if errRes != nil { | 151 if errRes != nil { |
| 151 return sr, log, errRes | 152 return sr, log, errRes |
| 152 } | 153 } |
| 153 return sr, log, errLog | 154 return sr, log, errLog |
| 154 } | 155 } |
| 155 | 156 |
| 156 // TODO(hinoka): This should go in a more generic file, when milo has more | |
| 157 // than one page. | |
| 158 func getNavi(taskID string, URL string) *resp.Navigation { | |
| 159 navi := &resp.Navigation{} | |
| 160 navi.PageTitle = &resp.Link{ | |
| 161 Label: taskID, | |
| 162 URL: URL, | |
| 163 } | |
| 164 navi.SiteTitle = &resp.Link{ | |
| 165 Label: "Milo", | |
| 166 URL: "/", | |
| 167 } | |
| 168 return navi | |
| 169 } | |
| 170 | |
| 171 // Given a logdog/milo step, translate it to a BuildComponent struct. | |
| 172 func miloBuildStep( | |
| 173 c context.Context, url string, anno *miloProto.Step, name string) *resp. BuildComponent { | |
| 174 url = strings.TrimSuffix(url, "/") | |
| 175 comp := &resp.BuildComponent{} | |
| 176 asc := anno.GetStepComponent() | |
| 177 comp.Label = asc.Name | |
| 178 switch asc.Status { | |
| 179 case miloProto.Status_RUNNING: | |
| 180 comp.Status = resp.Running | |
| 181 | |
| 182 case miloProto.Status_SUCCESS: | |
| 183 comp.Status = resp.Success | |
| 184 | |
| 185 case miloProto.Status_FAILURE: | |
| 186 if anno.GetFailureDetails() != nil { | |
| 187 switch anno.GetFailureDetails().Type { | |
| 188 case miloProto.FailureDetails_INFRA: | |
| 189 comp.Status = resp.InfraFailure | |
| 190 | |
| 191 case miloProto.FailureDetails_DM_DEPENDENCY_FAILED: | |
| 192 comp.Status = resp.DependencyFailure | |
| 193 | |
| 194 default: | |
| 195 comp.Status = resp.Failure | |
| 196 } | |
| 197 } else { | |
| 198 comp.Status = resp.Failure | |
| 199 } | |
| 200 | |
| 201 case miloProto.Status_EXCEPTION: | |
| 202 comp.Status = resp.InfraFailure | |
| 203 | |
| 204 // Missing the case of waiting on unfinished dependency... | |
| 205 default: | |
| 206 comp.Status = resp.NotRun | |
| 207 } | |
| 208 // Sub link is for one link per log that isn't stdio. | |
| 209 for _, link := range asc.GetOtherLinks() { | |
| 210 lds := link.GetLogdogStream() | |
| 211 if lds == nil { | |
| 212 logging.Warningf(c, "Warning: %v of %v has an empty logd og stream.", link, asc) | |
| 213 continue // DNE??? | |
| 214 } | |
| 215 shortName := lds.Name[5 : len(lds.Name)-2] | |
| 216 if strings.HasSuffix(lds.Name, "annotations") || strings.HasSuff ix(lds.Name, "stdio") { | |
| 217 // Skip the special ones. | |
| 218 continue | |
| 219 } | |
| 220 newLink := &resp.Link{ | |
| 221 Label: shortName, | |
| 222 URL: url + "/" + lds.Name, | |
| 223 } | |
| 224 comp.SubLink = append(comp.SubLink, newLink) | |
| 225 } | |
| 226 | |
| 227 // Main link is a link to the stdio. | |
| 228 comp.MainLink = &resp.Link{ | |
| 229 Label: "stdio", | |
| 230 URL: strings.Join([]string{url, name, "stdio"}, "/"), | |
| 231 } | |
| 232 | |
| 233 // This should always be a step. | |
| 234 comp.Type = resp.Step | |
| 235 | |
| 236 // This should always be 0 | |
| 237 comp.LevelsDeep = 0 | |
| 238 | |
| 239 // Timeswamapts | |
| 240 comp.Started = asc.Started.Time().Format(time.RFC3339) | |
| 241 | |
| 242 // This should be the exact same thing. | |
| 243 comp.Text = asc.Text | |
| 244 | |
| 245 return comp | |
| 246 } | |
| 247 | |
| 248 func taskProperties(sr *swarming.SwarmingRpcsTaskResult) *resp.PropertyGroup { | 157 func taskProperties(sr *swarming.SwarmingRpcsTaskResult) *resp.PropertyGroup { |
| 249 props := &resp.PropertyGroup{GroupName: "Swarming"} | 158 props := &resp.PropertyGroup{GroupName: "Swarming"} |
| 250 if len(sr.CostsUsd) == 1 { | 159 if len(sr.CostsUsd) == 1 { |
| 251 props.Property = append(props.Property, &resp.Property{ | 160 props.Property = append(props.Property, &resp.Property{ |
| 252 Key: "Cost of job (USD)", | 161 Key: "Cost of job (USD)", |
| 253 Value: fmt.Sprintf("$%.2f", sr.CostsUsd[0]), | 162 Value: fmt.Sprintf("$%.2f", sr.CostsUsd[0]), |
| 254 }) | 163 }) |
| 255 } | 164 } |
| 256 if sr.State == TaskCompleted || sr.State == TaskTimedOut { | 165 if sr.State == TaskCompleted || sr.State == TaskTimedOut { |
| 257 props.Property = append(props.Property, &resp.Property{ | 166 props.Property = append(props.Property, &resp.Property{ |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 273 Key: parts[0], | 182 Key: parts[0], |
| 274 } | 183 } |
| 275 if len(parts) == 2 { | 184 if len(parts) == 2 { |
| 276 p.Value = parts[1] | 185 p.Value = parts[1] |
| 277 } | 186 } |
| 278 props.Property = append(props.Property, p) | 187 props.Property = append(props.Property, p) |
| 279 } | 188 } |
| 280 return props | 189 return props |
| 281 } | 190 } |
| 282 | 191 |
| 283 func taskToBuild(c context.Context, sr *swarming.SwarmingRpcsTaskResult) (*resp. MiloBuild, error) { | 192 func taskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsTask Result) (*resp.MiloBuild, error) { |
| 284 » build := &resp.MiloBuild{} | 193 » build := &resp.MiloBuild{ |
| 194 » » Summary: resp.BuildComponent{ | |
| 195 » » » Source: &resp.Link{ | |
| 196 » » » » Label: "swarming task", | |
|
Ryan Tseng
2016/06/22 19:14:45
Label should be the taskID, or "Task %s"
nodir
2016/06/22 20:40:08
Done.
| |
| 197 » » » » URL: taskPageURL(server, sr.TaskId), | |
| 198 » » » }, | |
| 199 » » }, | |
| 200 » } | |
| 201 | |
| 285 switch sr.State { | 202 switch sr.State { |
| 286 case TaskRunning: | 203 case TaskRunning: |
| 287 build.Summary.Status = resp.Running | 204 build.Summary.Status = resp.Running |
| 288 | 205 |
| 289 case TaskPending: | 206 case TaskPending: |
| 290 build.Summary.Status = resp.NotRun | 207 build.Summary.Status = resp.NotRun |
| 291 | 208 |
| 292 case TaskExpired, TaskTimedOut, TaskBotDied: | 209 case TaskExpired, TaskTimedOut, TaskBotDied: |
| 293 build.Summary.Status = resp.InfraFailure | 210 build.Summary.Status = resp.InfraFailure |
| 294 | 211 |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 312 } | 229 } |
| 313 | 230 |
| 314 // Extract more swarming specific information into the properties. | 231 // Extract more swarming specific information into the properties. |
| 315 if props := taskProperties(sr); len(props.Property) > 0 { | 232 if props := taskProperties(sr); len(props.Property) > 0 { |
| 316 build.PropertyGroup = append(build.PropertyGroup, props) | 233 build.PropertyGroup = append(build.PropertyGroup, props) |
| 317 } | 234 } |
| 318 if props := tagsToProperties(sr.Tags); len(props.Property) > 0 { | 235 if props := tagsToProperties(sr.Tags); len(props.Property) > 0 { |
| 319 build.PropertyGroup = append(build.PropertyGroup, props) | 236 build.PropertyGroup = append(build.PropertyGroup, props) |
| 320 } | 237 } |
| 321 | 238 |
| 239 if sr.BotId != "" { | |
| 240 build.Summary.Bot = &resp.Link{ | |
| 241 Label: "swarming bot", | |
|
Ryan Tseng
2016/06/22 19:14:45
Label should just be the botID
nodir
2016/06/22 20:40:08
Done.
| |
| 242 URL: botPageURL(server, sr.BotId), | |
| 243 } | |
| 244 } | |
| 245 | |
| 322 // Build times. Swarming timestamps are UTC RFC3339Nano, but without the | 246 // Build times. Swarming timestamps are UTC RFC3339Nano, but without the |
| 323 // timezone information. Make them valid RFC3339Nano. | 247 // timezone information. Make them valid RFC3339Nano. |
| 324 build.Summary.Started = sr.StartedTs + "Z" | 248 build.Summary.Started = sr.StartedTs + "Z" |
| 325 if sr.CompletedTs != "" { | 249 if sr.CompletedTs != "" { |
| 326 build.Summary.Finished = sr.CompletedTs + "Z" | 250 build.Summary.Finished = sr.CompletedTs + "Z" |
| 327 } | 251 } |
| 328 if sr.Duration != 0 { | 252 if sr.Duration != 0 { |
| 329 build.Summary.Duration = uint64(sr.Duration) | 253 build.Summary.Duration = uint64(sr.Duration) |
| 330 } else if sr.State == TaskRunning { | 254 } else if sr.State == TaskRunning { |
| 331 started, err := time.Parse(time.RFC3339, build.Summary.Started) | 255 started, err := time.Parse(time.RFC3339, build.Summary.Started) |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 377 for _, t := range sr.Tags { | 301 for _, t := range sr.Tags { |
| 378 if t == "allow_milo:1" { | 302 if t == "allow_milo:1" { |
| 379 allowMilo = true | 303 allowMilo = true |
| 380 break | 304 break |
| 381 } | 305 } |
| 382 } | 306 } |
| 383 if !allowMilo { | 307 if !allowMilo { |
| 384 return nil, fmt.Errorf("Not A Milo Job") | 308 return nil, fmt.Errorf("Not A Milo Job") |
| 385 } | 309 } |
| 386 | 310 |
| 387 » build, err := taskToBuild(c, sr) | 311 » build, err := taskToBuild(c, server, sr) |
| 388 if err != nil { | 312 if err != nil { |
| 389 return nil, err | 313 return nil, err |
| 390 } | 314 } |
| 391 | 315 |
| 392 // Decode the data using annotee. The logdog stream returned here is ass umed | 316 // Decode the data using annotee. The logdog stream returned here is ass umed |
| 393 // to be consistent, which is why the following block of code are not | 317 // to be consistent, which is why the following block of code are not |
| 394 // expected to ever err out. | 318 // expected to ever err out. |
| 395 lds, err := streamsFromAnnotatedLog(c, body) | 319 lds, err := streamsFromAnnotatedLog(c, body) |
| 396 if err != nil { | 320 if err != nil { |
| 397 build.Components = []*resp.BuildComponent{{ | 321 build.Components = []*resp.BuildComponent{{ |
| 398 Type: resp.Summary, | 322 Type: resp.Summary, |
| 399 Label: "milo annotation parser", | 323 Label: "milo annotation parser", |
| 400 Text: []string{err.Error()}, | 324 Text: []string{err.Error()}, |
| 401 Status: resp.InfraFailure, | 325 Status: resp.InfraFailure, |
| 402 SubLink: []*resp.Link{{ | 326 SubLink: []*resp.Link{{ |
| 403 Label: "swarming task", | 327 Label: "swarming task", |
| 404 » » » » URL: taskPageURL(resolveServer(server), taskID ), | 328 » » » » URL: taskPageURL(server, taskID), |
| 405 }}, | 329 }}, |
| 406 }} | 330 }} |
| 407 } else { | 331 } else { |
| 408 logdog.AddLogDogToBuild(c, URL, lds, build) | 332 logdog.AddLogDogToBuild(c, URL, lds, build) |
| 409 } | 333 } |
| 410 | 334 |
| 411 return build, nil | 335 return build, nil |
| 412 } | 336 } |
| 413 | 337 |
| 414 // taskPageURL returns a URL to a human-consumable page of a swarming task. | 338 // taskPageURL returns a URL to a human-consumable page of a swarming task. |
| 339 // Supports server aliases. | |
| 415 func taskPageURL(swarmingHostname, taskID string) string { | 340 func taskPageURL(swarmingHostname, taskID string) string { |
| 416 » return fmt.Sprintf("https://%s/user/task/%s", swarmingHostname, taskID) | 341 » return fmt.Sprintf("https://%s/user/task/%s", resolveServer(swarmingHost name), taskID) |
| 417 } | 342 } |
| 343 | |
| 344 // botPageURL returns a URL to a human-consumable page of a swarming bot. | |
| 345 // Supports server aliases. | |
| 346 func botPageURL(swarmingHostname, botID string) string { | |
| 347 return fmt.Sprintf("https://%s/restricted/bot/%s", resolveServer(swarmin gHostname), botID) | |
| 348 } | |
| OLD | NEW |