go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/ftxtest/swarming.go (about) 1 // Copyright 2023 The Fuchsia Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style license that can 3 // found in the LICENSE file. 4 package main 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "os" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "time" 17 18 "go.chromium.org/luci/common/api/swarming/swarming/v1" 19 ftxproto "go.fuchsia.dev/infra/cmd/ftxtest/proto" 20 ) 21 22 type Swarming struct { 23 instance string 24 service *swarming.Service 25 cas *CAS 26 } 27 28 const ( 29 taskPriority = 200 30 taskExpiration = 5 * time.Hour 31 taskExecutionTimeout = 2 * time.Hour 32 poolTaskInterval = 10 * time.Second 33 defaultSummaryPath = "out/summary.json" 34 ) 35 36 func NewSwarming(httpClient *http.Client, instance string, cas *CAS) (*Swarming, error) { 37 swarmingService, err := swarming.New(httpClient) 38 swarmingService.BasePath = fmt.Sprintf("https://%s.appspot.com/_ah/api/swarming/v1/", instance) 39 if err != nil { 40 return nil, fmt.Errorf("swarming.New: %v", err) 41 } 42 swarming := &Swarming{ 43 service: swarmingService, 44 instance: instance, 45 cas: cas, 46 } 47 return swarming, nil 48 } 49 50 func (s *Swarming) LaunchTask(buildInput *ftxproto.InputProperties, parentTaskId string) (*swarming.SwarmingRpcsTaskRequestMetadata, error) { 51 casInput, err := s.casInput(buildInput) 52 if err != nil { 53 return nil, fmt.Errorf("casInput: %v", err) 54 } 55 hostTest := buildInput.Test.GetHost() 56 properties := &swarming.SwarmingRpcsTaskProperties{ 57 ExecutionTimeoutSecs: int64(taskExpiration.Seconds()), 58 Command: hostTest.Command, 59 CasInputRoot: casInput, 60 Outputs: []string{"out"}, 61 Dimensions: swarmmingStringPair(buildInput.TargetDimensions), 62 Env: swarmmingStringPair(hostTest.Env), 63 } 64 if len(buildInput.CipdPackages) > 0 { 65 properties.CipdInput = cipdInput(buildInput.CipdPackages) 66 } 67 return s.service.Tasks.New(&swarming.SwarmingRpcsNewTaskRequest{ 68 Name: buildInput.Name, 69 ParentTaskId: parentTaskId, 70 ServiceAccount: "internal-artifact-readers@fuchsia-infra.iam.gserviceaccount.com", 71 ExpirationSecs: int64(taskExpiration.Seconds()), 72 Priority: taskPriority, 73 Realm: realm(buildInput), 74 Properties: properties, 75 }).Do() 76 } 77 78 func (s *Swarming) WaitTask(taskId string) error { 79 for { 80 result, err := s.service.Task.Result(taskId).Fields("state", "failure", "internal_failure").Do() 81 if err != nil { 82 err = fmt.Errorf("swarming.Task.Result RPC failed: %v", err) 83 return err 84 } 85 if result.State == "PENDING" || result.State == "RUNNING" { 86 time.Sleep(poolTaskInterval) 87 continue 88 } 89 if result.State != "COMPLETED" || result.Failure || result.InternalFailure { 90 return fmt.Errorf("Task got %s want COMPLETED", result.State) 91 } 92 return nil 93 } 94 } 95 96 func (s *Swarming) casInput(buildInput *ftxproto.InputProperties) (*swarming.SwarmingRpcsCASReference, error) { 97 digestSplit := strings.Split(buildInput.InputArtifactsDigest, "/") 98 sizeBytes, err := strconv.ParseInt(digestSplit[1], 10, 64) 99 if err != nil { 100 return nil, fmt.Errorf("sizeBytes: %v", err) 101 } 102 return &swarming.SwarmingRpcsCASReference{ 103 CasInstance: fmt.Sprintf("projects/%s/instances/default_instance", s.instance), 104 Digest: &swarming.SwarmingRpcsDigest{ 105 Hash: digestSplit[0], 106 SizeBytes: sizeBytes, 107 }, 108 }, nil 109 } 110 111 func cipdInput(cipdPackages []*ftxproto.CipdPackage) *swarming.SwarmingRpcsCipdInput { 112 packages := []*swarming.SwarmingRpcsCipdPackage{} 113 for _, inputPkg := range cipdPackages { 114 packages = append(packages, &swarming.SwarmingRpcsCipdPackage{ 115 Path: inputPkg.Path, 116 Version: inputPkg.Version, 117 PackageName: inputPkg.Name, 118 }) 119 } 120 return &swarming.SwarmingRpcsCipdInput{ 121 Packages: packages, 122 } 123 } 124 125 func swarmmingStringPair(m map[string]string) []*swarming.SwarmingRpcsStringPair { 126 result := []*swarming.SwarmingRpcsStringPair{} 127 for key, value := range m { 128 result = append(result, &swarming.SwarmingRpcsStringPair{ 129 Key: key, 130 Value: value, 131 }) 132 } 133 return result 134 } 135 136 func realm(buildInput *ftxproto.InputProperties) string { 137 if buildInput.External { 138 return "fuchsia:try" 139 } else { 140 return "turquoise:global.try" 141 } 142 } 143 144 // testSummary determines the data for out/summary.json 145 type testSummary struct { 146 Success bool `json:"success"` 147 } 148 149 func (s *Swarming) CheckTestFailure(ctx context.Context, taskId string, buildInput *ftxproto.InputProperties, buildOutput *ftxproto.OutputProperties) error { 150 task, err := s.service.Task.Result(taskId).Do() 151 if err != nil { 152 return fmt.Errorf("task.request: %v", err) 153 } 154 if task.CasOutputRoot == nil || task.CasOutputRoot.Digest == nil { 155 return errors.New("Swarming task did not produce CAS output") 156 } 157 d := task.CasOutputRoot.Digest 158 buildOutput.OutputArtifactsDigest = fmt.Sprintf("%s/%d", d.Hash, d.SizeBytes) 159 dir, err := s.cas.Download(ctx, d.Hash, d.SizeBytes) 160 if err != nil { 161 return fmt.Errorf("cas.Download: %v", err) 162 } 163 summaryPath := defaultSummaryPath 164 if buildInput.SummaryPath != "" { 165 summaryPath = buildInput.SummaryPath 166 } 167 summary := testSummary{} 168 err = readJSON(filepath.Join(dir, summaryPath), &summary) 169 if err != nil { 170 171 return fmt.Errorf("readJSON: %v", err) 172 } 173 if !summary.Success { 174 return errors.New("Test failure") 175 } 176 return nil 177 } 178 179 func readJSON(filename string, out any) error { 180 rawData, err := os.ReadFile(filename) 181 if err != nil { 182 return fmt.Errorf("os.ReadFile: %v", err) 183 } 184 err = json.Unmarshal(rawData, out) 185 if err != nil { 186 return fmt.Errorf("json.Unmarshal: %v", err) 187 } 188 return nil 189 }