go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/artifacts/storetestoutputs.go (about) 1 // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "io" 13 "log" 14 "os" 15 "sync" 16 17 "github.com/google/subcommands" 18 "go.chromium.org/luci/auth" 19 "go.chromium.org/luci/auth/client/authcli" 20 "go.chromium.org/luci/hardcoded/chromeinfra" 21 "go.fuchsia.dev/infra/artifacts" 22 ) 23 24 const ( 25 // The maximum number of concurrent uploads. We rate limit this because we don't know 26 // how many entries there are in the input manifest file, so we don't know how many 27 // go-routines will be kicked off during execution. At around 100 or so concurrent 28 // uploads, the Golang storage API starts returning 400 errors and files fail to flush 29 // (when writer.Close() is called). Tweak this number as needed. 30 maxConcurrentUploads = 100 31 ) 32 33 // TestOutputsManifest describes how to upload test files. This is intentionally written 34 // to match the schema of Fuchsia's existing summary.json "tests" field for backward 35 // compatibility. We should migrate away from using a single output file for stdout and 36 // stderr and prefer uploading separate files. 37 type TestOutputsManifest = []TestOutputs 38 39 type TestOutputs struct { 40 Name string `json:"name"` 41 OutputFile string `json:"output_file"` 42 } 43 44 // StoreTestOutputsCommand performs a batch upload of test outputs to Cloud Storage. 45 type StoreTestOutputsCommand struct { 46 authFlags authcli.Flags 47 bucket string 48 build string 49 testEnv string 50 workers sync.WaitGroup 51 } 52 53 func (*StoreTestOutputsCommand) Name() string { 54 return "storetestoutputs" 55 } 56 57 func (*StoreTestOutputsCommand) Usage() string { 58 return fmt.Sprintf(`storetestoutputs [flags] outputs.json 59 60 The input manifest is a JSON list of objects with the following scheme: 61 62 { 63 "name": "The name of the test", 64 "output_file": "/path/to/test/output/file" 65 } 66 67 output_file is written to Cloud Storage as just %q within the hierarchy documented in 68 //tools/artifacts/doc.go.`, artifacts.DefaultTestOutputName) 69 } 70 71 func (*StoreTestOutputsCommand) Synopsis() string { 72 return fmt.Sprintf("stores test output files in Cloud Storage") 73 } 74 75 func (cmd *StoreTestOutputsCommand) SetFlags(f *flag.FlagSet) { 76 cmd.authFlags.Register(flag.CommandLine, chromeinfra.DefaultAuthOptions()) 77 f.StringVar(&cmd.bucket, "bucket", "", "The Cloud Storage bucket to write to") 78 f.StringVar(&cmd.build, "build", "", "The BuildBucket build ID") 79 f.StringVar(&cmd.testEnv, "testenv", "", "A canonical name for the test environment") 80 } 81 82 func (cmd *StoreTestOutputsCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...any) subcommands.ExitStatus { 83 opts, err := cmd.authFlags.Options() 84 if err != nil { 85 log.Println(err) 86 return subcommands.ExitFailure 87 } 88 if err := cmd.validateFlags(f); err != nil { 89 log.Println(err) 90 return subcommands.ExitFailure 91 } 92 manifest, err := cmd.parseManifestFile(f.Arg(0)) 93 if err != nil { 94 log.Println(err) 95 return subcommands.ExitFailure 96 } 97 if !cmd.execute(ctx, manifest, opts) { 98 return subcommands.ExitFailure 99 } 100 return subcommands.ExitSuccess 101 } 102 103 func (cmd *StoreTestOutputsCommand) parseManifestFile(path string) (TestOutputsManifest, error) { 104 bytes, err := os.ReadFile(path) 105 if err != nil { 106 return nil, fmt.Errorf("failed to read %q: %w", path, err) 107 } 108 var manifest TestOutputsManifest 109 if err := json.Unmarshal(bytes, &manifest); err != nil { 110 return nil, fmt.Errorf("fail to unmarshal manifest: %w", err) 111 } 112 return manifest, nil 113 } 114 115 func (cmd *StoreTestOutputsCommand) validateFlags(f *flag.FlagSet) error { 116 if f.NArg() != 1 { 117 return fmt.Errorf("expect exactly 1 positional argument") 118 } 119 if cmd.bucket == "" { 120 return fmt.Errorf("missing -bucket") 121 } 122 if cmd.build == "" { 123 return fmt.Errorf("missing -build") 124 } 125 if cmd.testEnv == "" { 126 return fmt.Errorf("missing -testenv") 127 } 128 return nil 129 } 130 131 // execute spawns a worker pool to perform the upload. The pool is limited in size by 132 // maxConcurrentUploads to avoid load failures in the storage API. 133 func (cmd *StoreTestOutputsCommand) execute(ctx context.Context, manifest TestOutputsManifest, opts auth.Options) bool { 134 const workerCount = maxConcurrentUploads 135 success := true 136 outputs := make(chan TestOutputs) 137 errs := make(chan error, workerCount) 138 for range workerCount { 139 cmd.workers.Add(1) 140 go cmd.worker(ctx, &cmd.workers, outputs, errs, opts) 141 } 142 for _, entry := range manifest { 143 outputs <- entry 144 } 145 close(outputs) 146 go func() { 147 for err := range errs { 148 success = false 149 log.Println(err) 150 } 151 }() 152 cmd.workers.Wait() 153 close(errs) 154 return success 155 } 156 157 func (cmd *StoreTestOutputsCommand) worker(ctx context.Context, wg *sync.WaitGroup, outputs <-chan TestOutputs, errs chan<- error, opts auth.Options) { 158 defer wg.Done() 159 cli, err := artifacts.NewClient(ctx, opts) 160 if err != nil { 161 errs <- err 162 return 163 } 164 dir := cli.GetBuildDir(cmd.bucket, cmd.build).(*artifacts.BuildDirectory) 165 for output := range outputs { 166 if err := cmd.upload(context.Background(), output, dir); err != nil { 167 errs <- err 168 } 169 } 170 } 171 172 func (cmd *StoreTestOutputsCommand) upload(ctx context.Context, outputs TestOutputs, dir *artifacts.BuildDirectory) error { 173 fd, err := os.Open(outputs.OutputFile) 174 if err != nil { 175 return fmt.Errorf("failed to read %q: %w", outputs.OutputFile, err) 176 } 177 object := dir.NewTestOutputObject(ctx, outputs.Name, cmd.testEnv) 178 writer := object.NewWriter(ctx) 179 if _, err := io.Copy(writer, fd); err != nil { 180 return fmt.Errorf("failed to write %q: %w", object.ObjectName(), err) 181 } 182 if err := writer.Close(); err != nil { 183 return fmt.Errorf("failed to flush bufferfor %q: %w", object.ObjectName(), err) 184 } 185 return nil 186 }