blob: b88dea86a59cd5ec5d8d08331c2a74b4e0c2f487 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"os"
"path/filepath"
"go.chromium.org/luci/common/clock"
"github.com/golang/protobuf/ptypes/timestamp"
"go.chromium.org/luci/luciexe/exe"
"go.chromium.org/luci/luciexe/invoke"
"go.fuchsia.dev/infra/cmd/build_init/checkout"
"go.chromium.org/luci/logdog/common/types"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
)
const (
// Root dir to ensure recipe bundle CIPD package.
cipdRoot = "rb"
// Default integration ref to fetch, if not passed through build properties.
defaultIntegrationRef = "HEAD"
// Top-level step name for a bootstrapped recipe execution.
namespace = "bootstrapped_recipe"
// Recipe manifest filename relative to root of an integration checkout.
recipesManifest = "infra/recipes"
// Recipe bundle CIPD package name to use.
recipesPackageName = "fuchsia/infra/recipe_bundles/fuchsia.googlesource.com/infra/recipes"
)
type bootstrapStepRunner struct {
build *buildbucketpb.Build
send exe.BuildSender
bootstrap *bootstrap.Bootstrap
}
// Calls a named step func, and attaches the step to sr.Build.Steps.
func (sr *bootstrapStepRunner) runStep(ctx context.Context, name string, fn stepFunc) error {
start := clock.Now(ctx).Unix()
err := fn(ctx, sr.build)
end := clock.Now(ctx).Unix()
// If there is an error, mark the step purple and attach a stderr log.
var status buildbucketpb.Status
logs := []*buildbucketpb.Log{}
if err != nil {
status = buildbucketpb.Status_INFRA_FAILURE
log, logErr := sr.getStderrLog(ctx, name, err)
if logErr != nil {
return logErr
}
logs = append(logs, log)
} else {
status = buildbucketpb.Status_SUCCESS
}
step := buildbucketpb.Step{
Name: name,
StartTime: &timestamp.Timestamp{Seconds: start},
EndTime: &timestamp.Timestamp{Seconds: end},
Status: status,
Logs: logs,
}
sr.build.Steps = append(sr.build.Steps, &step)
sr.send()
return err
}
// Calls a luciexe, attaching its steps to sr.Build.Steps.
func (sr *bootstrapStepRunner) invoke(ctx context.Context, exeArgs []string, opts *invoke.Options) error {
subprocess, err := invoke.Start(ctx, exeArgs, sr.build, opts)
if err != nil {
return err
}
// Add recipe execution steps to the build.
sr.build.Steps = append(sr.build.Steps, subprocess.Step)
sr.send()
_, err = subprocess.Wait()
return err
}
// Writes a step error to a LogDog stream and returns a Log.
func (sr *bootstrapStepRunner) getStderrLog(ctx context.Context, name string, stepErr error) (*buildbucketpb.Log, error) {
streamName, err := types.MakeStreamName("", name, "stderr")
if err != nil {
return nil, err
}
wc, err := sr.bootstrap.Client.NewTextStream(ctx, streamName)
defer wc.Close()
if err != nil {
return nil, err
}
if _, err := wc.Write([]byte(stepErr.Error())); err != nil {
return nil, err
}
streamAddr := types.StreamAddr{
Host: sr.bootstrap.CoordinatorHost,
Project: sr.bootstrap.Project,
Path: sr.bootstrap.Prefix.AsPathPrefix(streamName),
}
log := buildbucketpb.Log{
Name: "stderr",
Url: streamAddr.String(),
}
return &log, nil
}
// A stepFunc which resolves an integration checkout.
func resolveCheckout(ctx context.Context, build *buildbucketpb.Build) error {
integrationURL, err := resolveIntegrationURL(build.Input)
if err != nil {
return err
}
return checkout.Checkout(*build.Input, *integrationURL, defaultIntegrationRef)
}
// A stepFunc which resolves the recipes version in a checkout and downloads the bundle.
func resolveRecipeBundle(ctx context.Context, build *buildbucketpb.Build) error {
manifestXML, err := os.Open(recipesManifest)
if err != nil {
return err
}
defer manifestXML.Close()
pkg, err := resolveRecipesPackage(manifestXML)
if err != nil {
return err
}
return ensure(ctx, pkg, cipdRoot)
}
// BootstrapRecipe resolves the recipes version to use, downloads
// the recipe bundle from CIPD, and uses the bundle to run the build.
func bootstrapRecipe(ctx context.Context, sr stepRunner) error {
if err := sr.runStep(ctx, "resolve checkout", resolveCheckout); err != nil {
return err
}
if err := sr.runStep(ctx, "resolve recipe bundle", resolveRecipeBundle); err != nil {
return err
}
invokeOpts := invoke.Options{Namespace: namespace}
cwd, err := os.Getwd()
if err != nil {
return err
}
luciexePath := filepath.Join(cwd, cipdRoot, "luciexe")
// Invoke recipe bundle luciexe.
if err := sr.invoke(ctx, []string{luciexePath}, &invokeOpts); err != nil {
return err
}
return nil
}
func main() {
exe.Run(func(ctx context.Context, input *buildbucketpb.Build, userArgs []string, send exe.BuildSender) error {
logdogBootstrap, err := bootstrap.Get()
if err != nil {
return err
}
return bootstrapRecipe(ctx, &bootstrapStepRunner{
build: input,
send: send,
bootstrap: logdogBootstrap,
})
})
}