1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "flag" 12 "fmt" 13 "strconv" 14 "time" 15 16 "cloud.google.com/go/storage" 17 "google.golang.org/api/option" 18 19 "go.skia.org/infra/go/auth" 20 "go.skia.org/infra/go/gcs" 21 "go.skia.org/infra/go/gcs/gcsclient" 22 "go.skia.org/infra/go/httputils" 23 "go.skia.org/infra/go/skerr" 24 "go.skia.org/infra/go/sklog" 25 "go.skia.org/infra/task_driver/go/lib/auth_steps" 26 "go.skia.org/infra/task_driver/go/lib/checkout" 27 "go.skia.org/infra/task_driver/go/td" 28) 29 30const ( 31 g3CanaryBucketName = "g3-compile-tasks" 32 33 InfraFailureErrorMsg = "Your run failed due to unknown infrastructure failures. Ask the Infra Gardener to investigate (or directly ping rmistry@)." 34 MissingApprovalErrorMsg = "To run the G3 tryjob, changes must be either owned and authored by Googlers or approved (Code-Review+1) by Googlers." 35 MergeConflictErrorMsg = "G3 tryjob failed because the change is causing a merge conflict when applying it to the Skia hash in G3." 36 37 PatchingInformation = "Tip: If needed, could try patching in the CL into a local G3 client with \"g4 patch\" and then hacking on it." 38) 39 40type CanaryStatusType string 41 42const ( 43 ExceptionStatus CanaryStatusType = "exception" 44 MissingApprovalStatus CanaryStatusType = "missing_approval" 45 MergeConflictStatus CanaryStatusType = "merge_conflict" 46 FailureStatus CanaryStatusType = "failure" 47 SuccessStatus CanaryStatusType = "success" 48) 49 50type G3CanaryTask struct { 51 Issue int `json:"issue"` 52 Patchset int `json:"patchset"` 53 Status CanaryStatusType `json:"status"` 54 Result string `json:"result"` 55 Error string `json:"error"` 56 CL int `json:"cl"` 57} 58 59func main() { 60 var ( 61 projectId = flag.String("project_id", "", "ID of the Google Cloud project.") 62 taskId = flag.String("task_id", "", "ID of this task.") 63 taskName = flag.String("task_name", "", "Name of the task.") 64 output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.") 65 local = flag.Bool("local", true, "True if running locally (as opposed to on the bots)") 66 67 checkoutFlags = checkout.SetupFlags(nil) 68 ) 69 ctx := td.StartRun(projectId, taskId, taskName, output, local) 70 defer td.EndRun(ctx) 71 72 rs, err := checkout.GetRepoState(checkoutFlags) 73 if err != nil { 74 td.Fatal(ctx, skerr.Wrap(err)) 75 } 76 if rs.Issue == "" || rs.Patchset == "" { 77 td.Fatalf(ctx, "This task driver should be run only as a try bot") 78 } 79 80 // Create token source with scope for GCS access. 81 ts, err := auth_steps.Init(ctx, *local, auth.SCOPE_USERINFO_EMAIL, auth.SCOPE_FULL_CONTROL) 82 if err != nil { 83 td.Fatal(ctx, skerr.Wrap(err)) 84 } 85 client := httputils.DefaultClientConfig().WithTokenSource(ts).Client() 86 store, err := storage.NewClient(ctx, option.WithHTTPClient(client)) 87 if err != nil { 88 td.Fatalf(ctx, "Failed to create storage service client: %s", err) 89 } 90 gcsClient := gcsclient.New(store, g3CanaryBucketName) 91 92 taskFileName := fmt.Sprintf("%s-%s.json", rs.Issue, rs.Patchset) 93 taskStoragePath := fmt.Sprintf("gs://%s/%s", g3CanaryBucketName, taskFileName) 94 95 err = td.Do(ctx, td.Props("Trigger new task if not already running"), func(ctx context.Context) error { 96 if _, err := gcsClient.GetFileContents(ctx, taskFileName); err != nil { 97 if err == storage.ErrObjectNotExist { 98 // The task is not already running. Create a new file to trigger a new run. 99 if err := triggerCanaryRoll(ctx, rs.Issue, rs.Patchset, taskFileName, taskStoragePath, gcsClient); err != nil { 100 td.Fatal(ctx, fmt.Errorf("Could not trigger canary roll for %s/%s: %s", rs.Issue, rs.Patchset, err)) 101 } 102 } else { 103 return fmt.Errorf("Could not read %s: %s", taskStoragePath, err) 104 } 105 } else { 106 fmt.Printf("G3 canary task for %s/%s already exists\n", rs.Issue, rs.Patchset) 107 } 108 return nil 109 }) 110 if err != nil { 111 td.Fatal(ctx, skerr.Wrap(err)) 112 } 113 114 defer func() { 115 // Cleanup the storage file after the task finishes. 116 if err := gcsClient.DeleteFile(ctx, taskFileName); err != nil { 117 sklog.Errorf("Could not delete %s: %s", taskStoragePath, err) 118 } 119 }() 120 121 // Add documentation link for canary rolls. 122 td.StepText(ctx, "Canary roll doc", "https://goto.google.com/autoroller-canary-bots") 123 124 // Wait for the canary roll to finish. 125 if err := waitForCanaryRoll(ctx, taskFileName, taskStoragePath, gcsClient); err != nil { 126 td.Fatal(ctx, skerr.Wrap(err)) 127 } 128} 129 130func triggerCanaryRoll(ctx context.Context, issue, patchset, taskFileName, taskStoragePath string, gcsClient gcs.GCSClient) error { 131 ctx = td.StartStep(ctx, td.Props("Trigger canary roll")) 132 defer td.EndStep(ctx) 133 134 i, err := strconv.Atoi(issue) 135 if err != nil { 136 return fmt.Errorf("Could not convert %s to int: %s", issue, err) 137 } 138 p, err := strconv.Atoi(patchset) 139 if err != nil { 140 return fmt.Errorf("Could not convert %s to int: %s", patchset, err) 141 } 142 newTask := G3CanaryTask{ 143 Issue: i, 144 Patchset: p, 145 } 146 taskJson, err := json.Marshal(newTask) 147 if err != nil { 148 return fmt.Errorf("Could not encode task to JSON: %s", err) 149 } 150 if err := gcsClient.SetFileContents(ctx, taskFileName, gcs.FILE_WRITE_OPTS_TEXT, taskJson); err != nil { 151 return fmt.Errorf("Could not write task to %s: %s", taskStoragePath, err) 152 } 153 fmt.Printf("G3 canary task for %s/%s has been successfully added to %s\n", issue, patchset, taskStoragePath) 154 return nil 155} 156 157func waitForCanaryRoll(parentCtx context.Context, taskFileName, taskStoragePath string, gcsClient gcs.GCSClient) error { 158 ctx := td.StartStep(parentCtx, td.Props("Wait for canary roll")) 159 defer td.EndStep(ctx) 160 161 // For writing to the step's log stream. 162 stdout := td.NewLogStream(ctx, "stdout", td.SeverityInfo) 163 // Lets add the roll link only once to step data. 164 addedRollLinkStepData := false 165 for { 166 // Read task status from storage. 167 contents, err := gcsClient.GetFileContents(ctx, taskFileName) 168 if err != nil { 169 return td.FailStep(ctx, fmt.Errorf("Could not read contents of %s: %s", taskStoragePath, err)) 170 } 171 var task G3CanaryTask 172 if err := json.Unmarshal(contents, &task); err != nil { 173 return td.FailStep(ctx, fmt.Errorf("Could not unmarshal %s: %s", taskStoragePath, err)) 174 } 175 176 var rollStatus string 177 if task.CL == 0 { 178 rollStatus = "Waiting for Canary roll to start" 179 } else { 180 clLink := fmt.Sprintf("http://cl/%d", task.CL) 181 if !addedRollLinkStepData { 182 // Add the roll link to both the current step and it's parent. 183 td.StepText(ctx, "Canary roll CL", clLink) 184 td.StepText(parentCtx, "Canary roll CL", clLink) 185 addedRollLinkStepData = true 186 } 187 rollStatus = fmt.Sprintf("Canary roll [ %s ] has status %s", clLink, task.Result) 188 } 189 if _, err := stdout.Write([]byte(rollStatus)); err != nil { 190 return td.FailStep(ctx, fmt.Errorf("Could not write to stdout: %s", err)) 191 } 192 193 switch task.Status { 194 case "": 195 // Still waiting for the task. 196 time.Sleep(30 * time.Second) 197 continue 198 case ExceptionStatus: 199 if task.Error == "" { 200 return td.FailStep(ctx, fmt.Errorf("Run failed with: %s", task.Error)) 201 } else { 202 // Use a general purpose error message. 203 return td.FailStep(ctx, errors.New(InfraFailureErrorMsg)) 204 } 205 case MissingApprovalStatus: 206 return td.FailStep(ctx, errors.New(MissingApprovalErrorMsg)) 207 case MergeConflictStatus: 208 return td.FailStep(ctx, errors.New(MergeConflictErrorMsg)) 209 case FailureStatus: 210 return td.FailStep(ctx, fmt.Errorf("Run failed G3 TAP.\n%s", PatchingInformation)) 211 case SuccessStatus: 212 // Run passed G3 TAP. 213 return nil 214 } 215 } 216} 217