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
 }