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  }