[resultstore] Organize connection and environment code

* Put environment code in environment.go
* Connect() -> NewUploadClient()
* Add Flags and Options for parsing conn info from the
  command line.

Change-Id: I83c4da307a847c1024633c410dd87c6696c3a559
diff --git a/resultstore/resultstore.go b/resultstore/environment.go
similarity index 61%
rename from resultstore/resultstore.go
rename to resultstore/environment.go
index e083ab1..d0c12d3 100644
--- a/resultstore/resultstore.go
+++ b/resultstore/environment.go
@@ -4,42 +4,7 @@
 
 package resultstore
 
-import (
-	"context"
-	"crypto/x509"
-	"fmt"
-
-	api "google.golang.org/genproto/googleapis/devtools/resultstore/v2"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials"
-)
-
-var (
-	// Google Cloud API scope required to use ResultStore Upload API.
-	RequiredScopes = []string{
-		"https://www.googleapis.com/auth/cloud-platform",
-	}
-)
-
-// Connect returns a new UploadClient connected to the ResultStore backend at the given host.
-func Connect(ctx context.Context, environment Environment, creds credentials.PerRPCCredentials) (*UploadClient, error) {
-	pool, err := x509.SystemCertPool()
-	if err != nil {
-		return nil, fmt.Errorf("failed to create cert pool: %v", err)
-	}
-
-	transportCreds := credentials.NewClientTLSFromCert(pool, "")
-
-	conn, err := grpc.Dial(
-		environment.GRPCServiceAddress(),
-		grpc.WithTransportCredentials(transportCreds),
-		grpc.WithPerRPCCredentials(creds),
-	)
-	if err != nil {
-		return nil, err
-	}
-	return NewUploadClient(api.NewResultStoreUploadClient(conn)), nil
-}
+import "fmt"
 
 // Environment describes which ResultStore environment to use.
 type Environment string
diff --git a/resultstore/upload_client.go b/resultstore/upload_client.go
index 43bf5a2..02709bd 100644
--- a/resultstore/upload_client.go
+++ b/resultstore/upload_client.go
@@ -6,16 +6,107 @@
 
 import (
 	"context"
+	"crypto/x509"
+	"flag"
+	"fmt"
 	"log"
 
 	"github.com/google/uuid"
+	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/auth/client/authcli"
+	"go.chromium.org/luci/hardcoded/chromeinfra"
 	api "google.golang.org/genproto/googleapis/devtools/resultstore/v2"
 	"google.golang.org/genproto/protobuf/field_mask"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
 )
 
-// NewUploadClient creates a new UploadClient. This is visible for testing; Use `Connect` instead.
-func NewUploadClient(client api.ResultStoreUploadClient) *UploadClient {
-	return &UploadClient{client: client}
+// Returns the list of Google Cloud API scopes required to use the ResultStore Upload API.
+func requiredScopes() []string {
+	return []string{"https://www.googleapis.com/auth/cloud-platform"}
+}
+
+// UploadClientFlags are used to parse commnad-line options for creating a new ResultStore
+// UploadClient. The options are, in turn, used to generate UploadClientOptions. Example:
+//
+//  var cf UploadClientFlags
+//
+//  func init() {
+//    cf.Register(flag.CommandLine)
+//  }
+//
+//  func Main(ctx context.Background()) {
+//    flag.Parse()
+//    opts, err := cf.Options(ctx)
+//    // check err ...
+//    client, err := NewClient(ctx, opts)
+//    // check err ...
+//  }
+type UploadClientFlags struct {
+	authFlags authcli.Flags
+	environ   Environment
+}
+
+// Register sets these UploadClient flags on the given flag.FlagSet.
+func (f *UploadClientFlags) Register(in *flag.FlagSet) {
+	// LUCI auth flags
+	defaultAuthOpts := chromeinfra.DefaultAuthOptions()
+	defaultAuthOpts.Scopes = append(defaultAuthOpts.Scopes, requiredScopes()...)
+	f.authFlags.Register(in, defaultAuthOpts)
+
+	// ResultStore flags.
+	environs := []Environment{Production, Staging}
+	in.Var(&f.environ, "environment", fmt.Sprintf("ResultStore environment: %v", environs))
+}
+
+// Options returns UploadClientOptions created from this UploadClientFlags' inputs.
+func (f *UploadClientFlags) Options() (*UploadClientOptions, error) {
+	authOpts, err := f.authFlags.Options()
+	if err != nil {
+		return nil, fmt.Errorf("failed to create LUCI auth options: %v", err)
+	}
+	return &UploadClientOptions{
+		Environ:  f.environ,
+		AuthOpts: authOpts,
+	}, nil
+}
+
+// UploadClientOptions are used to create new UploadClients. See UploadClientFlags for
+// example usage.
+type UploadClientOptions struct {
+	Environ  Environment
+	AuthOpts auth.Options
+}
+
+// NewClient returns a new UploadClient connected to a ResultStore backend.
+func NewClient(ctx context.Context, opts UploadClientOptions) (*UploadClient, error) {
+	// Generate transport credentials.
+	pool, err := x509.SystemCertPool()
+	if err != nil {
+		return nil, fmt.Errorf("failed to create cert pool: %v", err)
+	}
+	tcreds, err := credentials.NewClientTLSFromCert(pool, ""), nil
+	if err != nil {
+		return nil, err
+	}
+	// Generate per RPC credentials.
+	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts.AuthOpts)
+	pcreds, err := authenticator.PerRPCCredentials()
+	if err != nil {
+		return nil, err
+	}
+	conn, err := grpc.Dial(
+		opts.Environ.GRPCServiceAddress(),
+		grpc.WithTransportCredentials(tcreds),
+		grpc.WithPerRPCCredentials(pcreds),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &UploadClient{
+		client: api.NewResultStoreUploadClient(conn),
+		conn:   conn,
+	}, nil
 }
 
 // UploadClient wraps the ResultStoreUpload client libraries.
@@ -24,10 +115,18 @@
 // Context object contains a non-empty string value for TestUUIDKey, that value is
 // used instead. This is done by calling `SetTestUUID(ctx, "uuid")`.
 //
-// UploadClient requires an Invocation's authorization token to be set in the provided Context.
-// This can be done by calling: `SetAuthToken(ctx, "auth-token")`.
+// UploadClient requires an Invocation's authorization token to be set in the provided
+// Context. This can be done by calling: `SetAuthToken(ctx, "auth-token")`.
+//
+// The user should Close() the client when finished.
 type UploadClient struct {
 	client api.ResultStoreUploadClient
+	conn   *grpc.ClientConn
+}
+
+// Close closes this UploadClient's connection to ResultStore.
+func (c *UploadClient) Close() error {
+	return c.conn.Close()
 }
 
 // CreateInvocation creates an Invocation in ResultStore. This must be called before
diff --git a/resultstore/upload_client_test.go b/resultstore/upload_client_test.go
index 2e72f77..8e831c5 100644
--- a/resultstore/upload_client_test.go
+++ b/resultstore/upload_client_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package resultstore_test
+package resultstore
 
 import (
 	"context"
@@ -10,10 +10,8 @@
 	"testing"
 	"time"
 
-	"fuchsia.googlesource.com/tools/resultstore"
 	"fuchsia.googlesource.com/tools/resultstore/mocks"
 	"github.com/golang/mock/gomock"
-
 	api "google.golang.org/genproto/googleapis/devtools/resultstore/v2"
 	"google.golang.org/genproto/protobuf/field_mask"
 )
@@ -37,7 +35,7 @@
 )
 
 type tester struct {
-	client *resultstore.UploadClient
+	client *UploadClient
 	mock   *mocks.MockResultStoreUploadClient
 }
 
@@ -58,13 +56,13 @@
 		{
 			method:      "CreateConfiguration",
 			description: "should rmake an RPC to create a Configuration",
-			output: &resultstore.Configuration{
+			output: &Configuration{
 				Name:         "resultstore_configuration_name",
 				ID:           "configuration_id",
 				InvocationID: "invocation_id",
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.Configuration{
+				input := &Configuration{
 					ID:           "configuration_id",
 					InvocationID: "invocation_id",
 					Properties:   map[string]string{"key": "value"},
@@ -94,24 +92,24 @@
 		{
 			method:      "CreateConfiguredTarget",
 			description: "should make an RPC to create a ConfiguredTarget",
-			output: &resultstore.ConfiguredTarget{
+			output: &ConfiguredTarget{
 				Name: "resultstore_configured_target_name",
-				ID: &resultstore.ConfiguredTargetID{
+				ID: &ConfiguredTargetID{
 					InvocationID: "invocation_id",
 					ConfigID:     "configuration_id",
 					TargetID:     "target_id",
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.ConfiguredTarget{
-					ID: &resultstore.ConfiguredTargetID{
+				input := &ConfiguredTarget{
+					ID: &ConfiguredTargetID{
 						InvocationID: "invocation_id",
 						TargetID:     "target_id",
 						ConfigID:     "configuration_id",
 					},
 					Properties: map[string]string{"key": "value"},
 					StartTime:  may18_1993,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.ConfiguredTarget{
@@ -139,12 +137,12 @@
 		{
 			method:      "CreateInvocation",
 			description: "should make an RPC to create an Invocation",
-			output: &resultstore.Invocation{
+			output: &Invocation{
 				Name: "resultstore_invocation_name",
 				ID:   "invocation_id",
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.Invocation{
+				input := &Invocation{
 					ProjectID:  "123456789",
 					ID:         "invocation_id",
 					Users:      []string{"user"},
@@ -152,7 +150,7 @@
 					Properties: map[string]string{"key": "value"},
 					LogURL:     "http://test.log",
 					StartTime:  may18_1993,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Invocation{
@@ -177,21 +175,21 @@
 		{
 			method:      "CreateTarget",
 			description: "should make an RPC to create a Target",
-			output: &resultstore.Target{
+			output: &Target{
 				Name: "resultstore_target_name",
-				ID: &resultstore.TargetID{
+				ID: &TargetID{
 					ID:           "target_id",
 					InvocationID: "invocation_id",
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.Target{
-					ID: &resultstore.TargetID{
+				input := &Target{
+					ID: &TargetID{
 						ID: "target_id",
 					},
 					Properties: map[string]string{"key": "value"},
 					StartTime:  may18_1993,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Target{
@@ -218,17 +216,17 @@
 		{
 			method:      "CreateTestAction",
 			description: "should make an RPC to create a Test Action",
-			output: &resultstore.TestAction{
+			output: &TestAction{
 				Name: "resultstore_action_name",
-				ID: &resultstore.TestActionID{
+				ID: &TestActionID{
 					InvocationID: "invocation_id",
 					ConfigID:     "configuration_id",
 					TargetID:     "target_id",
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.TestAction{
-					ID: &resultstore.TestActionID{
+				input := &TestAction{
+					ID: &TestActionID{
 						ID:           "test",
 						InvocationID: "invocation_id",
 						TargetID:     "target_id",
@@ -237,7 +235,7 @@
 					TestSuite:  "test_suite",
 					TestLogURI: "http://test.log",
 					StartTime:  may18_1993,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Action{
@@ -308,23 +306,23 @@
 		{
 			method:      "UpdateConfiguredTarget",
 			description: "should make an RPC to update a ConfiguredTarget",
-			output: &resultstore.ConfiguredTarget{
+			output: &ConfiguredTarget{
 				Name: "resultstore_configured_target_name",
-				ID: &resultstore.ConfiguredTargetID{
+				ID: &ConfiguredTargetID{
 					InvocationID: "invocation_id",
 					ConfigID:     "configuration_id",
 					TargetID:     "target_id",
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.ConfiguredTarget{
-					ID: &resultstore.ConfiguredTargetID{
+				input := &ConfiguredTarget{
+					ID: &ConfiguredTargetID{
 						InvocationID: "invocation_id",
 						TargetID:     "target_id",
 						ConfigID:     "configuration_id",
 					},
 					Properties: map[string]string{"key": "value"},
-					Status:     resultstore.Passed,
+					Status:     Passed,
 					StartTime:  may18_1993,
 					Duration:   time.Hour,
 				}
@@ -354,12 +352,12 @@
 		{
 			method:      "UpdateInvocation",
 			description: "should make an RPC to update an Invocation",
-			output: &resultstore.Invocation{
+			output: &Invocation{
 				Name: "resultstore_invocation_name",
 				ID:   "invocation_id",
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.Invocation{
+				input := &Invocation{
 					ID:         "invocation_id",
 					Properties: map[string]string{"key": "value"},
 					ProjectID:  "project_id",
@@ -368,7 +366,7 @@
 					Users:      []string{"users"},
 					Labels:     []string{"label"},
 					LogURL:     "url",
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Invocation{
@@ -394,23 +392,23 @@
 		{
 			method:      "UpdateTarget",
 			description: "should make an RPC to update a Target",
-			output: &resultstore.Target{
+			output: &Target{
 				Name: "resultstore_target_name",
-				ID: &resultstore.TargetID{
+				ID: &TargetID{
 					ID:           "target_id",
 					InvocationID: "invocation_id",
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.Target{
-					ID: &resultstore.TargetID{
+				input := &Target{
+					ID: &TargetID{
 						ID:           "target_id",
 						InvocationID: "invocation_id",
 					},
 					Properties: map[string]string{"key": "value"},
 					StartTime:  may18_1993,
 					Duration:   time.Hour,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Target{
@@ -437,9 +435,9 @@
 		{
 			method:      "UpdateTestAction",
 			description: "should make an RPC to update a Test Action",
-			output: &resultstore.TestAction{
+			output: &TestAction{
 				Name: "resultstore_action_name",
-				ID: &resultstore.TestActionID{
+				ID: &TestActionID{
 					ID:           "action_id",
 					InvocationID: "invocation_id",
 					ConfigID:     "configuration_id",
@@ -447,8 +445,8 @@
 				},
 			},
 			execute: func(ctx context.Context, tester *tester) (interface{}, error) {
-				input := &resultstore.TestAction{
-					ID: &resultstore.TestActionID{
+				input := &TestAction{
+					ID: &TestActionID{
 						ID:           "test",
 						InvocationID: "invocation_id",
 						TargetID:     "target_id",
@@ -458,7 +456,7 @@
 					TestLogURI: "http://test.log",
 					StartTime:  may18_1993,
 					Duration:   time.Hour,
-					Status:     resultstore.Passed,
+					Status:     Passed,
 				}
 
 				response := &api.Action{
@@ -486,20 +484,20 @@
 		},
 	}
 
-	setup := func(t *testing.T) (context.Context, *resultstore.UploadClient, *mocks.MockResultStoreUploadClient, *gomock.Controller) {
-		ctx, err := resultstore.SetTestUUID(context.Background(), testUUID)
+	setup := func(t *testing.T) (context.Context, *UploadClient, *mocks.MockResultStoreUploadClient, *gomock.Controller) {
+		ctx, err := SetTestUUID(context.Background(), testUUID)
 		if err != nil {
 			t.Fatalf("failed to set test uuid: %v", err)
 		}
 
-		ctx, err = resultstore.SetAuthToken(ctx, testAuthToken)
+		ctx, err = SetAuthToken(ctx, testAuthToken)
 		if err != nil {
 			t.Fatalf("failed to set test auth token: %v", err)
 		}
 
 		controller := gomock.NewController(t)
 		mock := mocks.NewMockResultStoreUploadClient(controller)
-		client := resultstore.NewUploadClient(mock)
+		client := &UploadClient{client: mock}
 		return ctx, client, mock, controller
 	}