[buildbucket] Add a BuildBucket wrapper library

IN-990 #comment

TEST: go test ./..

Change-Id: Ic3f7b2d8ec0fd74e0b6b3d37ed89747d83d92752
diff --git a/buildbucket/build.go b/buildbucket/build.go
new file mode 100644
index 0000000..f7fce73
--- /dev/null
+++ b/buildbucket/build.go
@@ -0,0 +1,28 @@
+// 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 buildbucket
+
+import (
+	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
+)
+
+// Build is a helper for reading Buildbucket Build information.
+type Build buildbucketpb.Build
+
+// GitilesCommit returns the Gitiles commit that triggered this build, or nil if this
+// build was not triggered by a Gitiltes commit.
+func (b Build) GitilesCommit() *buildbucketpb.GitilesCommit {
+	return b.Input.GitilesCommit
+}
+
+// Property reads a specific input Property from this builder. Returns false with a nil
+// Property if not found.
+func (b Build) Property(name string) (*Property, bool) {
+	prop, ok := b.Input.Properties.Fields[name]
+	if !ok {
+		return nil, false
+	}
+	return &Property{name: name, value: prop}, true
+}
diff --git a/buildbucket/builder.go b/buildbucket/builder.go
new file mode 100644
index 0000000..5e5c09b
--- /dev/null
+++ b/buildbucket/builder.go
@@ -0,0 +1,45 @@
+// 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 buildbucket
+
+import (
+	"flag"
+	"fmt"
+	"strings"
+
+	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
+)
+
+// builderIDFlag identifies a Builder. This is a convenience type for reading a builder id
+// from command line flags.
+type builderIDFlag buildbucketpb.BuilderID
+
+func (b builderIDFlag) String() string {
+	return fmt.Sprintf("%s/%s/%s", b.Project, b.Bucket, b.Builder)
+}
+
+// Set implements flag.Value
+func (b *builderIDFlag) Set(input string) error {
+	parts := strings.SplitN(input, "/", 3)
+	if len(parts) != 3 {
+		return fmt.Errorf("invalid builder: %s must have form 'project/bucket/builder'", input)
+	}
+
+	b.Project = parts[0]
+	b.Bucket = parts[1]
+	b.Builder = parts[2]
+	return nil
+}
+
+// Get returns the parsed flag value. The output can be cast as a buildbucketpb.BuilderID.
+func (b builderIDFlag) Get() interface{} {
+	return buildbucketpb.BuilderID(b)
+}
+
+// BuilderID returns a flag.Value for reading a builder ID from a string.  The format of
+// the input is project/bucket/build.
+func BuilderID() flag.Getter {
+	return &builderIDFlag{}
+}
diff --git a/buildbucket/builder_test.go b/buildbucket/builder_test.go
new file mode 100644
index 0000000..41664a1
--- /dev/null
+++ b/buildbucket/builder_test.go
@@ -0,0 +1,135 @@
+// 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 buildbucket
+
+import (
+	"flag"
+	"reflect"
+	"testing"
+
+	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
+)
+
+func TestBuilderIDGetter(t *testing.T) {
+	tests := []struct {
+		// The name of this test case
+		name string
+
+		// The string to parse into a BuilderID
+		input string
+
+		// The expected result.
+		output buildbucketpb.BuilderID
+
+		// Whether to expect an error
+		expectErr bool
+	}{
+		{
+			name:  "should parse an input string into a BuilderID",
+			input: "project/bucket/builder",
+			output: buildbucketpb.BuilderID{
+				Project: "project",
+				Bucket:  "bucket",
+				Builder: "builder",
+			},
+		}, {
+			name:      "should err when the input contains < 2 fields",
+			expectErr: true,
+			output:    buildbucketpb.BuilderID{},
+		}, {
+			name:      "should err when the input is empty",
+			expectErr: true,
+			output:    buildbucketpb.BuilderID{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			value := BuilderID()
+			err := value.Set(tt.input)
+			if err != nil != tt.expectErr {
+				if tt.expectErr {
+					t.Error("expected an err but got nil")
+				} else {
+					t.Errorf("wanted %v but got an err: %v", tt.output, err)
+				}
+			}
+
+			// Compare flag values directly rather than using reflect.DeepEqual to prove
+			// that `Get` returns a valid object.
+			builderID := value.Get().(buildbucketpb.BuilderID)
+			if !reflect.DeepEqual(builderID, tt.output) {
+				t.Errorf("got\n%v\nbut wanted:\n%v", builderID, tt.output)
+			}
+		})
+	}
+}
+
+func TestBuilderIDString(t *testing.T) {
+	tests := []struct {
+		// The name of this test case
+		name string
+
+		// The input BuilderID
+		input flag.Value
+
+		// The expected string.
+		output string
+
+		// Whether to expect an error
+		expectErr bool
+	}{
+		{
+			name: "should format the ID as a string",
+			input: &builderIDFlag{
+				Project: "project",
+				Bucket:  "bucket",
+				Builder: "builder",
+			},
+			output: "project/bucket/builder",
+		}, {
+			name: "when the ID is empty",
+			input: &builderIDFlag{
+				Project: "",
+				Bucket:  "",
+				Builder: "",
+			},
+			output: "//",
+		}, {
+			name: "when Project is empty",
+			input: &builderIDFlag{
+				Project: "",
+				Bucket:  "bucket",
+				Builder: "builder",
+			},
+			output: "/bucket/builder",
+		}, {
+			name: "when Bucket is empty",
+			input: &builderIDFlag{
+				Project: "project",
+				Bucket:  "",
+				Builder: "builder",
+			},
+			output: "project//builder",
+		}, {
+			name: "when Builder is empty",
+			input: &builderIDFlag{
+				Project: "project",
+				Bucket:  "bucket",
+				Builder: "",
+			},
+			output: "project/bucket/",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			output := tt.input.String()
+			if output != tt.output {
+				t.Errorf("got %q but wanted %q", output, tt.output)
+			}
+		})
+	}
+}
diff --git a/buildbucket/builds.go b/buildbucket/builds.go
new file mode 100644
index 0000000..5952e14
--- /dev/null
+++ b/buildbucket/builds.go
@@ -0,0 +1,31 @@
+// 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 buildbucket
+
+import (
+	"context"
+	"fmt"
+
+	"go.chromium.org/luci/auth"
+	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
+	"go.chromium.org/luci/grpc/prpc"
+)
+
+// DefaultHost is the default Buildbucket server.
+const DefaultHost = "cr-buildbucket.appspot.com"
+
+// NewBuildsClient returns a new BuildsClient.
+func NewBuildsClient(ctx context.Context, host string, opts auth.Options) (buildbucketpb.BuildsClient, error) {
+	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts)
+	httpClient, err := authenticator.Client()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get authenticated http client: %v", err)
+	}
+
+	return buildbucketpb.NewBuildsPRPCClient(&prpc.Client{
+		C:    httpClient,
+		Host: host,
+	}), nil
+}
diff --git a/buildbucket/property.go b/buildbucket/property.go
new file mode 100644
index 0000000..395b365
--- /dev/null
+++ b/buildbucket/property.go
@@ -0,0 +1,28 @@
+// 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 buildbucket
+
+import (
+	structpb "github.com/golang/protobuf/ptypes/struct"
+)
+
+// Property is a Build input property.
+//
+// Implement different "TypedValue" methods on this type as needed.
+type Property struct {
+	name  string
+	value *structpb.Value
+}
+
+// Name returns the name of this property.
+func (p Property) Name() string {
+	return p.name
+}
+
+// StringValue returns the value of this property as a string. Returns the empty string if
+// the value is unset or is not a string.
+func (p Property) StringValue() string {
+	return p.value.GetStringValue()
+}
diff --git a/go.mod b/go.mod
index 391052d..f3aaa7b 100644
--- a/go.mod
+++ b/go.mod
@@ -9,9 +9,10 @@
 	github.com/googleapis/gax-go v2.0.2+incompatible // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
 	github.com/jtolds/gls v4.2.1+incompatible // indirect
+	github.com/julienschmidt/httprouter v1.2.0 // indirect
 	github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
 	github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
-	go.chromium.org/luci v0.0.0-20181218015242-20acb618582d
+	go.chromium.org/luci v0.0.0-20181004001148-1bfb80352368
 	go.opencensus.io v0.19.0 // indirect
 	golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
 	golang.org/x/net v0.0.0-20181217023233-e147a9138326
diff --git a/go.sum b/go.sum
index 6c28b65..a87a6ef 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,5 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
 git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
@@ -22,6 +23,8 @@
 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
@@ -36,6 +39,8 @@
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
+go.chromium.org/luci v0.0.0-20181004001148-1bfb80352368 h1:ijyFyFRPye+it8+bvyo09R1zrqgThy8/8PJxuWquPMg=
+go.chromium.org/luci v0.0.0-20181004001148-1bfb80352368/go.mod h1:MIQewVTLvOvc0UioV0JNqTNO/RspKFS0XEeoKrOxsdM=
 go.chromium.org/luci v0.0.0-20181218015242-20acb618582d h1:WWlp6PQtC8FyaxytRO5UBYFBDcPOYy6+o7JmcvgLMuU=
 go.chromium.org/luci v0.0.0-20181218015242-20acb618582d/go.mod h1:MIQewVTLvOvc0UioV0JNqTNO/RspKFS0XEeoKrOxsdM=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -54,6 +59,7 @@
 golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4=
 golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -67,6 +73,7 @@
 golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
 google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=