golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/cloudbuild.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package task 6 7 import ( 8 "context" 9 "fmt" 10 "io/fs" 11 "math/rand" 12 "strings" 13 14 cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" 15 "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" 16 "cloud.google.com/go/storage" 17 "golang.org/x/build/internal/gcsfs" 18 ) 19 20 type CloudBuildClient interface { 21 // RunBuildTrigger runs an existing trigger in project with the given 22 // substitutions. 23 RunBuildTrigger(ctx context.Context, project, trigger string, substitutions map[string]string) (CloudBuild, error) 24 // RunScript runs the given script under bash -eux -o pipefail in 25 // ScriptProject. Outputs are collected into the build's ResultURL, 26 // readable with ResultFS. The script will have the latest version of Go 27 // and some version of gsutil on $PATH. 28 // If gerritProject is specified, the script will run in the root of a 29 // checkout of the tip version of that repository. 30 RunScript(ctx context.Context, script string, gerritProject string, outputs []string) (CloudBuild, error) 31 // Completed reports whether a build has finished, returning an error if 32 // it's failed. It's suitable for use with AwaitCondition. 33 Completed(ctx context.Context, build CloudBuild) (detail string, completed bool, _ error) 34 // ResultFS returns an FS that contains the results of the given build. 35 ResultFS(ctx context.Context, build CloudBuild) (fs.FS, error) 36 } 37 38 type RealCloudBuildClient struct { 39 BuildClient *cloudbuild.Client 40 StorageClient *storage.Client 41 ScriptProject string 42 ScriptAccount string 43 ScratchURL string 44 } 45 46 // CloudBuild represents a Cloud Build that can be queried with the status 47 // methods on CloudBuildClient. 48 type CloudBuild struct { 49 Project, ID string 50 ResultURL string 51 } 52 53 func (c *RealCloudBuildClient) RunBuildTrigger(ctx context.Context, project, trigger string, substitutions map[string]string) (CloudBuild, error) { 54 op, err := c.BuildClient.RunBuildTrigger(ctx, &cloudbuildpb.RunBuildTriggerRequest{ 55 ProjectId: project, 56 TriggerId: trigger, 57 Source: &cloudbuildpb.RepoSource{ 58 Substitutions: substitutions, 59 }, 60 }) 61 if err != nil { 62 return CloudBuild{}, err 63 } 64 if _, err = op.Poll(ctx); err != nil { 65 return CloudBuild{}, err 66 } 67 meta, err := op.Metadata() 68 if err != nil { 69 return CloudBuild{}, err 70 } 71 return CloudBuild{Project: project, ID: meta.Build.Id}, nil 72 } 73 74 func (c *RealCloudBuildClient) RunScript(ctx context.Context, script string, gerritProject string, outputs []string) (CloudBuild, error) { 75 const downloadGoScript = `#!/usr/bin/env bash 76 set -eux 77 archive=$(wget -qO - 'https://go.dev/dl/?mode=json' | grep -Eo 'go.*linux-amd64.tar.gz' | head -n 1) 78 wget -qO - https://go.dev/dl/${archive} | tar -xz 79 mv go /workspace/released_go 80 ` 81 82 const scriptPrefix = `#!/usr/bin/env bash 83 set -eux 84 set -o pipefail 85 export PATH=/workspace/released_go/bin:$PATH 86 ` 87 88 // Cloud build loses directory structure when it saves artifacts, which is 89 // a problem since (e.g.) we have multiple files named go.mod in the 90 // tagging tasks. It's not very complicated, so reimplement it ourselves. 91 resultURL := fmt.Sprintf("%v/script-build-%v", c.ScratchURL, rand.Int63()) 92 var saveOutputsScript strings.Builder 93 saveOutputsScript.WriteString(scriptPrefix) 94 for _, out := range outputs { 95 saveOutputsScript.WriteString(fmt.Sprintf("gsutil cp %q %q\n", out, resultURL+"/"+strings.TrimPrefix(out, "./"))) 96 } 97 98 var steps []*cloudbuildpb.BuildStep 99 var dir string 100 if gerritProject != "" { 101 steps = append(steps, &cloudbuildpb.BuildStep{ 102 Name: "gcr.io/cloud-builders/git", 103 Args: []string{"clone", "https://go.googlesource.com/" + gerritProject, "checkout"}, 104 }) 105 dir = "checkout" 106 } 107 108 build := &cloudbuildpb.Build{ 109 Steps: append(steps, 110 &cloudbuildpb.BuildStep{ 111 Name: "bash", 112 Script: downloadGoScript, 113 }, 114 &cloudbuildpb.BuildStep{ 115 Name: "gcr.io/cloud-builders/gsutil", 116 Script: scriptPrefix + script, 117 Dir: dir, 118 }, 119 &cloudbuildpb.BuildStep{ 120 Name: "gcr.io/cloud-builders/gsutil", 121 Script: saveOutputsScript.String(), 122 Dir: dir, 123 }, 124 ), 125 Options: &cloudbuildpb.BuildOptions{ 126 MachineType: cloudbuildpb.BuildOptions_E2_HIGHCPU_8, 127 Logging: cloudbuildpb.BuildOptions_CLOUD_LOGGING_ONLY, 128 }, 129 ServiceAccount: c.ScriptAccount, 130 } 131 op, err := c.BuildClient.CreateBuild(ctx, &cloudbuildpb.CreateBuildRequest{ 132 ProjectId: c.ScriptProject, 133 Build: build, 134 }) 135 if err != nil { 136 return CloudBuild{}, fmt.Errorf("creating build: %w", err) 137 } 138 if _, err = op.Poll(ctx); err != nil { 139 return CloudBuild{}, fmt.Errorf("polling: %w", err) 140 } 141 meta, err := op.Metadata() 142 if err != nil { 143 return CloudBuild{}, fmt.Errorf("reading metadata: %w", err) 144 } 145 return CloudBuild{Project: c.ScriptProject, ID: meta.Build.Id, ResultURL: resultURL}, nil 146 147 } 148 149 func (c *RealCloudBuildClient) Completed(ctx context.Context, build CloudBuild) (string, bool, error) { 150 b, err := c.BuildClient.GetBuild(ctx, &cloudbuildpb.GetBuildRequest{ 151 ProjectId: build.Project, 152 Id: build.ID, 153 }) 154 if err != nil { 155 return "", false, err 156 } 157 if b.FinishTime == nil { 158 return "", false, nil 159 } 160 if b.Status != cloudbuildpb.Build_SUCCESS { 161 return "", false, fmt.Errorf("build %q failed, see %v: %v", build.ID, build.ResultURL, b.FailureInfo) 162 } 163 return b.StatusDetail, true, nil 164 } 165 166 func (c *RealCloudBuildClient) ResultFS(ctx context.Context, build CloudBuild) (fs.FS, error) { 167 return gcsfs.FromURL(ctx, c.StorageClient, build.ResultURL) 168 }