Add support for uploading to the Catapult dashboard
IN-199
Change-Id: Idde4a589dc7681f4ef8a825d81ff386904f38c71
diff --git a/catapult/cmd/catapult/upload.go b/catapult/cmd/catapult/upload.go
index ce1b3cb..99476e9 100644
--- a/catapult/cmd/catapult/upload.go
+++ b/catapult/cmd/catapult/upload.go
@@ -1,34 +1,141 @@
-// Copyright 2017 The Fuchsia Authors. All rights reserved.
+// Copyright 2018 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 (
+ "bytes"
"context"
+ "encoding/json"
+ "errors"
"flag"
+ "fmt"
+ "io/ioutil"
"log"
+ "net/http"
+ "os"
+ "time"
+
+ "go.chromium.org/luci/client/authcli"
+ "go.chromium.org/luci/common/auth"
"github.com/google/subcommands"
)
-type UploadCommand struct{}
+// UploadCommand uploads a data file to a URL when executed.
+type UploadCommand struct {
+ // The timeout for HTTP requests.
+ timeout time.Duration
+
+ // The URL to upload data to
+ url string
+
+ // LUCI flags used to parse command-line authentication options.
+ authFlags authcli.Flags
+}
func (*UploadCommand) Name() string {
return "upload"
}
func (*UploadCommand) Usage() string {
- return "upload"
+ return "upload [options] json_file"
}
func (*UploadCommand) Synopsis() string {
- return "Uploads data to catapult"
+ return "Uploads a JSON file to a URL"
}
-func (*UploadCommand) SetFlags(flags *flag.FlagSet) {}
+func (cmd *UploadCommand) SetFlags(flags *flag.FlagSet) {
+ cmd.authFlags = authcli.Flags{}
+ cmd.authFlags.Register(flags, auth.Options{})
+ flags.DurationVar(&cmd.timeout, "timeout", 10*time.Second,
+ "Request timeout duration string. e.g. 12s or 1m")
+ flags.StringVar(&cmd.url, "url", "", "(required) The URL to upload data to")
+}
-func (*UploadCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
- log.Println("Unimplemented: Upload")
- return subcommands.ExitFailure
+func (cmd *UploadCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+ if f.NArg() < 1 {
+ fmt.Fprintln(os.Stderr, "missing input file")
+ return subcommands.ExitFailure
+ }
+ if f.NArg() != 1 {
+ fmt.Fprintln(os.Stderr, "too many positional arguments")
+ return subcommands.ExitFailure
+ }
+ if len(cmd.url) == 0 {
+ fmt.Fprintln(os.Stderr, "url is required")
+ return subcommands.ExitFailure
+ }
+ if cmd.timeout <= 0 {
+ fmt.Fprintf(os.Stderr, "timeout must be positive. Got %v\n", cmd.timeout)
+ return subcommands.ExitFailure
+ }
+
+ opts, err := cmd.authFlags.Options()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return subcommands.ExitFailure
+ }
+
+ inputFile := f.Arg(0)
+ if err := uploadData(ctx, cmd.url, inputFile, cmd.timeout, opts); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return subcommands.ExitFailure
+ }
+
+ return subcommands.ExitSuccess
+}
+
+func uploadData(ctx context.Context, url string, filepath string, timeout time.Duration, authOpts auth.Options) error {
+ requestBody, err := ioutil.ReadFile(filepath)
+ if err != nil {
+ return err
+ }
+
+ // Verify that the input data is actual JSON. Ideally we'd verify the
+ // structure of the data also but this isn't always practical. (For
+ // example, when parsing a dynamic schema such as Catapult's
+ // HistogramSet).
+ if !json.Valid(requestBody) {
+ return errors.New("input is not valid JSON")
+ }
+
+ req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(requestBody))
+ if err != nil {
+ return err
+ }
+
+ // The LUCI authenticator used to authenticate the request. Silent login
+ // prevents an interactive login session when running from CI bots.
+ authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, authOpts)
+
+ // Generate the OAuth token used to authenticate the request.
+ oauthToken, err := authenticator.GetAccessToken(time.Minute)
+ if err != nil {
+ return err
+ }
+
+ // Set an auth header on the request, containing the token generated from the
+ // provided service account information.
+ oauthToken.SetAuthHeader(req)
+
+ client, err := authenticator.Client()
+ if err != nil {
+ return err
+ }
+ client.Timeout = timeout
+
+ response, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if response.StatusCode < 200 || response.StatusCode >= 300 {
+ return errors.New(response.Status)
+ }
+
+ log.Println(response.Status)
+ return nil
}