blob: 2b9f06022945eef25bcc255dc60ee4c92baeea7e [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"bytes"
"context"
stderrors "errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"time"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
logdogbootstrap "go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
"go.chromium.org/luci/lucictx"
"go.chromium.org/luci/luciexe"
"infra/chromium/bootstrapper/bootstrap"
"infra/chromium/bootstrapper/clients/gclient"
"infra/chromium/bootstrapper/clients/gerrit"
"infra/chromium/bootstrapper/clients/gitiles"
)
type getOptionsFn func() options
func parseFlags() options {
outputPath := flag.String("output", "", "Path to write the final build.proto state to.")
polymorphic := flag.Bool("polymorphic", false, "Whether the builder bootstraps properties for other builders instead of itself; polymorphic builders give precedence to build properties rather than the properties in the properties file")
propertiesOptional := flag.Bool("properties-optional", false, "Whether missing $bootstrap/properties property should be allowed")
flag.Parse()
return options{
outputPath: *outputPath,
packagesRoot: "packages",
polymorphic: *polymorphic,
propertiesOptional: *propertiesOptional,
}
}
func getBuild(ctx context.Context, input io.Reader) (*buildbucketpb.Build, error) {
logging.Infof(ctx, "reading build input")
data, err := ioutil.ReadAll(input)
if err != nil {
return nil, errors.Annotate(err, "failed to read build input").Err()
}
logging.Infof(ctx, "unmarshalling build input")
build := &buildbucketpb.Build{}
if err = proto.Unmarshal(data, build); err != nil {
return nil, errors.Annotate(err, "failed to unmarshall build").Err()
}
return build, nil
}
type options struct {
outputPath string
packagesRoot string
polymorphic bool
propertiesOptional bool
}
type bootstrapFn func(ctx context.Context, input io.Reader, opts options) ([]string, []byte, error)
func performBootstrap(ctx context.Context, input io.Reader, opts options) ([]string, []byte, error) {
build, err := getBuild(ctx, input)
if err != nil {
return nil, nil, err
}
logging.Infof(ctx, "creating bootstrap input")
inputOpts := bootstrap.InputOptions{
Polymorphic: opts.polymorphic,
PropertiesOptional: opts.propertiesOptional,
}
bootstrapInput, err := inputOpts.NewInput(build)
if err != nil {
return nil, nil, err
}
var config *bootstrap.BootstrapConfig
var exe *bootstrap.BootstrappedExe
var cmd []string
// Downloading the necessary packages and getting the appropriate properties both speak to
// external services but don't necessarily depend on each other, so use an errgroup to do
// them in parallel
// Introduce a new block to shadow the ctx variable so that the outer
// value can't be used accidentally
{
group, ctx := errgroup.WithContext(ctx)
// If the builder's properties are in a dependent project, getting the properties
// might require the gclient binary from the depot_tools package, so provide a
// channel that can be used to synchronize where necessary
depotToolsCh := make(chan string, 1)
group.Go(func() error {
logging.Infof(ctx, "downloading necessary packages")
var err error
exe, cmd, err = bootstrap.DownloadPackages(ctx, bootstrapInput, opts.packagesRoot, map[string]chan<- string{
bootstrap.DepotToolsId: depotToolsCh,
})
return errors.Annotate(err, "failed to download necessary packages").Err()
})
group.Go(func() error {
// gclientGetter will only be called if dependency_project is set in the
// $bootstrap/exe property, depot_tools will always be downloaded in that
// case
gclientGetter := func(ctx context.Context) (*gclient.Client, error) {
var depotToolsPackagePath string
select {
case depotToolsPackagePath = <-depotToolsCh:
case <-ctx.Done():
return nil, ctx.Err()
}
gclientPath := filepath.Join(depotToolsPackagePath, "depot_tools", "gclient")
return gclient.NewClient(gclientPath), nil
}
bootstrapper := bootstrap.NewBuildBootstrapper(gitiles.NewClient(ctx), gerrit.NewClient(ctx), gclientGetter)
logging.Infof(ctx, "getting bootstrapped config")
var err error
config, err = bootstrapper.GetBootstrapConfig(ctx, bootstrapInput)
return err
})
if err := group.Wait(); err != nil {
return nil, nil, err
}
}
logging.Infof(ctx, "updating build")
err = config.UpdateBuild(build, exe)
if err != nil {
return nil, nil, err
}
logging.Infof(ctx, "marshalling bootstrapped build input")
recipeInput, err := proto.Marshal(build)
if err != nil {
return nil, nil, errors.Annotate(err, "failed to marshall bootstrapped build input: <%s>", build).Err()
}
if opts.outputPath != "" {
cmd = append(cmd, "--output", opts.outputPath)
}
return cmd, recipeInput, nil
}
type executeCmdFn func(ctx context.Context, cmd []string, input []byte) error
func executeCmd(ctx context.Context, cmd []string, input []byte) error {
cmdCtx := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
cmdCtx.Stdin = bytes.NewBuffer(input)
cmdCtx.Stdout = os.Stdout
cmdCtx.Stderr = os.Stderr
return cmdCtx.Run()
}
type updateBuildFn func(ctx context.Context, build *buildbucketpb.Build) error
func updateBuild(ctx context.Context, build *buildbucketpb.Build) (err error) {
outputData, err := proto.Marshal(build)
if err != nil {
return errors.Annotate(err, "failed to marshal output build.proto").Err()
}
logdog, err := logdogbootstrap.Get()
if err != nil {
return errors.Annotate(err, "failed to get logdog bootstrap instance").Err()
}
stream, err := logdog.Client.NewDatagramStream(
ctx,
luciexe.BuildProtoStreamSuffix,
streamclient.WithContentType(luciexe.BuildProtoContentType),
)
if err != nil {
return errors.Annotate(err, "failed to get datagram stream").Err()
}
defer func() {
closeErr := stream.Close()
if closeErr != nil {
if err != nil {
logging.Errorf(ctx, closeErr.Error())
} else {
err = closeErr
}
}
}()
err = stream.WriteDatagram(outputData)
if err != nil {
err = errors.Annotate(err, "failed to write modified build").Err()
return
}
return
}
func bootstrapMain(ctx context.Context, getOpts getOptionsFn, performBootstrap bootstrapFn, executeCmd executeCmdFn, updateBuild updateBuildFn) (time.Duration, error) {
opts := getOpts()
cmd, input, err := performBootstrap(ctx, os.Stdin, opts)
if err == nil {
logging.Infof(ctx, "executing %s", cmd)
err = executeCmd(ctx, cmd, input)
// An ExitError indicates that we were able to bootstrap the executable and that it
// failed, as opposed to being unable to launch the bootstrapped executable. In that
// case, the recipe will have run and we don't need to make any modifications to the
// build.
var exitErr *exec.ExitError
if stderrors.As(err, &exitErr) {
return 0, err
}
}
if err != nil {
logging.Errorf(ctx, err.Error())
build := &buildbucketpb.Build{}
if bootstrap.PatchRejected.In(err) {
build.Status = buildbucketpb.Status_FAILURE
build.SummaryMarkdown = "<pre>Patch failure: See build stderr log. Try rebasing?</pre>"
build.Output = &buildbucketpb.Build_Output{
Properties: &structpb.Struct{
Fields: map[string]*structpb.Value{
"failure_type": structpb.NewStringValue("PATCH_FAILURE"),
},
},
}
} else {
build.Status = buildbucketpb.Status_INFRA_FAILURE
build.SummaryMarkdown = fmt.Sprintf("<pre>%s</pre>", err)
}
if err := updateBuild(ctx, build); err != nil {
logging.Errorf(ctx, errors.Annotate(err, "failed to update build with failure details").Err().Error())
}
sleepDuration, _ := bootstrap.SleepBeforeExiting.In(err)
return sleepDuration, err
}
return 0, nil
}
func main() {
ctx := context.Background()
ctx = gologger.StdConfig.Use(ctx)
// Tracking soft deadline and calling shutdown causes the bootstrapper
// to participate in the termination protocol. No explicit action is
// necessary to terminate the bootstrapped executable, the signal will
// be propagated to the entire process/console group.
ctx, shutdown := lucictx.TrackSoftDeadline(ctx, 500*time.Millisecond)
defer shutdown()
sleepDuration, err := bootstrapMain(ctx, parseFlags, performBootstrap, executeCmd, updateBuild)
time.Sleep(sleepDuration)
if err != nil {
os.Exit(1)
}
}