• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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