| // 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: ×tamp.Timestamp{Seconds: start}, |
| EndTime: ×tamp.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, |
| }) |
| }) |
| } |