| // 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 reads a recipe spec by creating a checkout of the specified repository, |
| // according to the current build input. Clients should run this in the directory where |
| // they want the repository to be checked out. The spec is printed to stdout on success. |
| // On failure, a non-zero exit code is returned. |
| // |
| // This MUST be called after the input Buildbucket build has been resolved to contain a |
| // Gitiles commit. The build.proto object written to stdin must be base64 encoded to avoid |
| // logging invalid UTF-8 characters which breaks logdog. |
| // |
| // The exit codes of this program are as follows: |
| // 0: Ok |
| // 1: Unexpected fatal error |
| // 2: The spec file was not found |
| package main |
| |
| import ( |
| "context" |
| "encoding/base64" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net/url" |
| "os" |
| |
| "github.com/golang/protobuf/proto" |
| |
| "fuchsia.googlesource.com/infra/infra/cmd/build_init/checkout" |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| ) |
| |
| const ( |
| // The directory containing specs in the spec repository. |
| defaultSpecDir = "infra/config/generated" |
| defaultGitPath = "/usr/bin/git" |
| ) |
| |
| // Exit codes |
| const ( |
| ExitOk = 0 |
| ExitFatal = 1 |
| ExitFileNotFound = 2 |
| ) |
| |
| // Command line flags. |
| var ( |
| // The URL of the spec repository. |
| specRemote string |
| |
| // The directory containing specs in the spec repository. |
| specDir string |
| ) |
| |
| // Variables derived from user input. |
| var ( |
| // The URL to the spec repository. |
| specRepoURL *url.URL |
| |
| // The build.proto object read from stdin. |
| build *buildbucketpb.Build |
| ) |
| |
| func init() { |
| // Required |
| flag.StringVar(&specRemote, "spec_remote", "", "The URL of the spec repository") |
| |
| // Optional |
| flag.StringVar(&specDir, "spec_dir", defaultSpecDir, "The directory containing specs") |
| } |
| |
| func main() { |
| flag.Parse() |
| if err := validateInputs(); err != nil { |
| log.Fatal(err) |
| } |
| os.Exit(execute(context.Background())) |
| } |
| |
| func validateInputs() (err error) { |
| if specRemote == "" { |
| return errors.New("missing -spec_remote") |
| } |
| |
| specRepoURL, err = url.Parse(specRemote) |
| if err != nil { |
| return fmt.Errorf("invalid URL for -spec_remote: %v", err) |
| } |
| |
| build, err = loadBuildProto(os.Stdin) |
| if err != nil { |
| return fmt.Errorf("failed to read build.proto: %v", err) |
| } |
| if err := validateBuild(*build); err != nil { |
| return fmt.Errorf("invalid build message: %v", err) |
| } |
| return err |
| } |
| |
| func validateBuild(build buildbucketpb.Build) error { |
| if build.Input == nil { |
| return errors.New("build has no input") |
| } |
| if build.Input.GitilesCommit == nil { |
| return errors.New("build input has no Gitiles commit") |
| } |
| if build.Builder == nil { |
| return errors.New("input build has no builder information") |
| } |
| if build.Builder.Bucket == "" { |
| return errors.New("input build's builder has no bucket") |
| } |
| if build.Builder.Builder == "" { |
| return errors.New("input build's builder name is empty") |
| } |
| return nil |
| } |
| |
| // Execute runs the program. Public for testing. |
| func execute(ctx context.Context) (code int) { |
| if err := checkout.Checkout(*build.Input, *specRepoURL); err != nil { |
| log.Printf("failed to checkout %s: %v", specRepoURL.String(), err) |
| return ExitFatal |
| } |
| |
| specPath := fmt.Sprintf("%s/%s/%s", specDir, build.Builder.Bucket, build.Builder.Builder) |
| if _, err := os.Stat(specPath); os.IsNotExist(err) { |
| log.Printf("file not found %v", specPath) |
| return ExitFileNotFound |
| } |
| fd, err := os.Open(specPath) |
| if err != nil { |
| log.Printf("failed to open file %v: %v", specPath, err) |
| return ExitFatal |
| } |
| defer fd.Close() |
| if _, err := io.Copy(os.Stdout, fd); err != nil { |
| log.Printf("failed to write file to output: %v", err) |
| return ExitFatal |
| } |
| |
| return ExitOk |
| } |
| |
| // Loads a buildbucketpb.Build message from the given reader. The input must be base64. |
| func loadBuildProto(r io.Reader) (*buildbucketpb.Build, error) { |
| build := new(buildbucketpb.Build) |
| decoder := base64.NewDecoder(base64.StdEncoding, r) |
| bytes, err := ioutil.ReadAll(decoder) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read build.proto: %v", err) |
| } |
| if err := proto.Unmarshal(bytes, build); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal build.proto: %v", err) |
| } |
| return build, nil |
| } |