cmd/coordinator,cmd/retrybuilds: add wipe API to coordinator

As part of our migration to combine codebases of the Build Dashboard and
the Coordinator, the first step is to start calling a Coordinator API
for wiping release status of failed builds. This adds a gRPC API to the
coordinator, listening on the same port as the HTTPS listeners.

The Coordinator API in this implementation simply validates and forwards
a request to the dashboard API.

This change also updates cmd/retrybuilds to optionally use the
Coordinator gRPC API for wiping.

Tested locally using the live Dashboard API.

Updates golang/go#34744

Change-Id: I4b34b064625193eb11a280565d701605064a8443
Reviewed-on: https://go-review.googlesource.com/c/build/+/219120
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 310b93a..ae3b025 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -45,7 +45,9 @@
 	"unicode"
 
 	"go4.org/syncutil"
-	grpc "grpc.go4.org"
+	"golang.org/x/build/cmd/coordinator/protos"
+	"google.golang.org/grpc"
+	grpc4 "grpc.go4.org"
 
 	"cloud.google.com/go/errorreporting"
 	"cloud.google.com/go/storage"
@@ -162,8 +164,9 @@
 }
 
 func serveTLS(ln net.Listener) {
+	// Support HTTP/2 for gRPC handlers.
 	config := &tls.Config{
-		NextProtos: []string{"http/1.1"},
+		NextProtos: []string{"http/1.1", "h2"},
 	}
 
 	if autocertManager != nil {
@@ -240,6 +243,9 @@
 // autocertManager is non-nil if LetsEncrypt is in use.
 var autocertManager *autocert.Manager
 
+// grpcServer is a shared gRPC server. It is global, as it needs to be used in places that aren't factored otherwise.
+var grpcServer = grpc.NewServer()
+
 func main() {
 	flag.Parse()
 
@@ -293,12 +299,14 @@
 
 	addHealthCheckers(context.Background())
 
-	cc, err := grpc.NewClient(http.DefaultClient, "https://maintner.golang.org")
+	cc, err := grpc4.NewClient(http.DefaultClient, "https://maintner.golang.org")
 	if err != nil {
 		log.Fatal(err)
 	}
 	maintnerClient = apipb.NewMaintnerServiceClient(cc)
 
+	gs := &gRPCServer{dashboardURL: "https://build.golang.org"}
+	protos.RegisterCoordinatorServer(grpcServer, gs)
 	http.HandleFunc("/", handleStatus)
 	http.HandleFunc("/debug/goroutines", handleDebugGoroutines)
 	http.HandleFunc("/debug/watcher/", handleDebugWatcher)
diff --git a/cmd/coordinator/protos/coordinator.pb.go b/cmd/coordinator/protos/coordinator.pb.go
new file mode 100644
index 0000000..29f99e2
--- /dev/null
+++ b/cmd/coordinator/protos/coordinator.pb.go
@@ -0,0 +1,209 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: coordinator.proto
+
+package protos
+
+import (
+	context "context"
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+	math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// ClearResultsRequest specifies the data needed to clear a result.
+type ClearResultsRequest struct {
+	// builder is the builder to clear results.
+	Builder string `protobuf:"bytes,1,opt,name=builder,proto3" json:"builder,omitempty"`
+	// hash is the commit hash to clear results.
+	Hash                 string   `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ClearResultsRequest) Reset()         { *m = ClearResultsRequest{} }
+func (m *ClearResultsRequest) String() string { return proto.CompactTextString(m) }
+func (*ClearResultsRequest) ProtoMessage()    {}
+func (*ClearResultsRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_99e779eb11ceee19, []int{0}
+}
+
+func (m *ClearResultsRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ClearResultsRequest.Unmarshal(m, b)
+}
+func (m *ClearResultsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ClearResultsRequest.Marshal(b, m, deterministic)
+}
+func (m *ClearResultsRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ClearResultsRequest.Merge(m, src)
+}
+func (m *ClearResultsRequest) XXX_Size() int {
+	return xxx_messageInfo_ClearResultsRequest.Size(m)
+}
+func (m *ClearResultsRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_ClearResultsRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ClearResultsRequest proto.InternalMessageInfo
+
+func (m *ClearResultsRequest) GetBuilder() string {
+	if m != nil {
+		return m.Builder
+	}
+	return ""
+}
+
+func (m *ClearResultsRequest) GetHash() string {
+	if m != nil {
+		return m.Hash
+	}
+	return ""
+}
+
+type ClearResultsResponse struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *ClearResultsResponse) Reset()         { *m = ClearResultsResponse{} }
+func (m *ClearResultsResponse) String() string { return proto.CompactTextString(m) }
+func (*ClearResultsResponse) ProtoMessage()    {}
+func (*ClearResultsResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_99e779eb11ceee19, []int{1}
+}
+
+func (m *ClearResultsResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_ClearResultsResponse.Unmarshal(m, b)
+}
+func (m *ClearResultsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_ClearResultsResponse.Marshal(b, m, deterministic)
+}
+func (m *ClearResultsResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ClearResultsResponse.Merge(m, src)
+}
+func (m *ClearResultsResponse) XXX_Size() int {
+	return xxx_messageInfo_ClearResultsResponse.Size(m)
+}
+func (m *ClearResultsResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_ClearResultsResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ClearResultsResponse proto.InternalMessageInfo
+
+func init() {
+	proto.RegisterType((*ClearResultsRequest)(nil), "protos.ClearResultsRequest")
+	proto.RegisterType((*ClearResultsResponse)(nil), "protos.ClearResultsResponse")
+}
+
+func init() { proto.RegisterFile("coordinator.proto", fileDescriptor_99e779eb11ceee19) }
+
+var fileDescriptor_99e779eb11ceee19 = []byte{
+	// 151 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4c, 0xce, 0xcf, 0x2f,
+	0x4a, 0xc9, 0xcc, 0x4b, 0x2c, 0xc9, 0x2f, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x03,
+	0x53, 0xc5, 0x4a, 0xce, 0x5c, 0xc2, 0xce, 0x39, 0xa9, 0x89, 0x45, 0x41, 0xa9, 0xc5, 0xa5, 0x39,
+	0x25, 0xc5, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x12, 0x5c, 0xec, 0x49, 0xa5, 0x99,
+	0x39, 0x29, 0xa9, 0x45, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x30, 0xae, 0x90, 0x10, 0x17,
+	0x4b, 0x46, 0x62, 0x71, 0x86, 0x04, 0x13, 0x58, 0x18, 0xcc, 0x56, 0x12, 0xe3, 0x12, 0x41, 0x35,
+	0xa4, 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0xd5, 0x28, 0x8a, 0x8b, 0xdb, 0x19, 0x61, 0xb3, 0x90, 0x37,
+	0x17, 0x0f, 0xb2, 0x32, 0x21, 0x69, 0x88, 0x5b, 0x8a, 0xf5, 0xb0, 0xb8, 0x40, 0x4a, 0x06, 0xbb,
+	0x24, 0xc4, 0x64, 0x25, 0x86, 0x24, 0x88, 0x07, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x10,
+	0xa0, 0xb6, 0xbb, 0xdc, 0x00, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConnInterface
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion6
+
+// CoordinatorClient is the client API for Coordinator service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type CoordinatorClient interface {
+	// ClearResults clears build failures from the coordinator to force them to rebuild.
+	ClearResults(ctx context.Context, in *ClearResultsRequest, opts ...grpc.CallOption) (*ClearResultsResponse, error)
+}
+
+type coordinatorClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewCoordinatorClient(cc grpc.ClientConnInterface) CoordinatorClient {
+	return &coordinatorClient{cc}
+}
+
+func (c *coordinatorClient) ClearResults(ctx context.Context, in *ClearResultsRequest, opts ...grpc.CallOption) (*ClearResultsResponse, error) {
+	out := new(ClearResultsResponse)
+	err := c.cc.Invoke(ctx, "/protos.Coordinator/ClearResults", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// CoordinatorServer is the server API for Coordinator service.
+type CoordinatorServer interface {
+	// ClearResults clears build failures from the coordinator to force them to rebuild.
+	ClearResults(context.Context, *ClearResultsRequest) (*ClearResultsResponse, error)
+}
+
+// UnimplementedCoordinatorServer can be embedded to have forward compatible implementations.
+type UnimplementedCoordinatorServer struct {
+}
+
+func (*UnimplementedCoordinatorServer) ClearResults(ctx context.Context, req *ClearResultsRequest) (*ClearResultsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ClearResults not implemented")
+}
+
+func RegisterCoordinatorServer(s *grpc.Server, srv CoordinatorServer) {
+	s.RegisterService(&_Coordinator_serviceDesc, srv)
+}
+
+func _Coordinator_ClearResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ClearResultsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(CoordinatorServer).ClearResults(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/protos.Coordinator/ClearResults",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(CoordinatorServer).ClearResults(ctx, req.(*ClearResultsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+var _Coordinator_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "protos.Coordinator",
+	HandlerType: (*CoordinatorServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "ClearResults",
+			Handler:    _Coordinator_ClearResults_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "coordinator.proto",
+}
diff --git a/cmd/coordinator/protos/coordinator.proto b/cmd/coordinator/protos/coordinator.proto
new file mode 100644
index 0000000..1f1d6b7
--- /dev/null
+++ b/cmd/coordinator/protos/coordinator.proto
@@ -0,0 +1,22 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+syntax = "proto3";
+
+package protos;
+
+service Coordinator {
+  // ClearResults clears build failures from the coordinator to force them to rebuild.
+  rpc ClearResults(ClearResultsRequest) returns (ClearResultsResponse) {}
+}
+
+// ClearResultsRequest specifies the data needed to clear a result.
+message ClearResultsRequest {
+  // builder is the builder to clear results.
+  string builder = 1;
+  // hash is the commit hash to clear results.
+  string hash = 2;
+}
+
+message ClearResultsResponse {}
diff --git a/cmd/coordinator/protos/protos.go b/cmd/coordinator/protos/protos.go
new file mode 100644
index 0000000..d186447
--- /dev/null
+++ b/cmd/coordinator/protos/protos.go
@@ -0,0 +1,11 @@
+// Copyright 2020 The Go 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 protos
+
+// Run "go generate" in this directory to update. You need to have:
+//
+// - a protoc binary (see https://github.com/golang/protobuf#installation)
+// - go get -u github.com/golang/protobuf/protoc-gen-go
+
+//go:generate protoc --proto_path=$GOPATH/src:. --go_out=plugins=grpc:. coordinator.proto
diff --git a/cmd/coordinator/results.go b/cmd/coordinator/results.go
new file mode 100644
index 0000000..09e6472
--- /dev/null
+++ b/cmd/coordinator/results.go
@@ -0,0 +1,155 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.13
+// +build linux darwin
+
+// Code related to the Build Results API.
+
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"golang.org/x/build/cmd/coordinator/protos"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/metadata"
+	grpcstatus "google.golang.org/grpc/status"
+)
+
+type gRPCServer struct {
+	// embed an UnimplementedCoordinatorServer to avoid errors when adding new RPCs to the proto.
+	*protos.UnimplementedCoordinatorServer
+
+	// dashboardURL is the base URL of the Dashboard service (https://build.golang.org)
+	dashboardURL string
+}
+
+// ClearResults implements the ClearResults RPC call from the CoordinatorService.
+//
+// It currently hits the build Dashboard service to clear a result.
+// TODO(golang.org/issue/34744) - Change to wipe build status from the Coordinator itself after findWork
+// starts using maintner.
+func (g *gRPCServer) ClearResults(ctx context.Context, req *protos.ClearResultsRequest) (*protos.ClearResultsResponse, error) {
+	key, err := keyFromContext(ctx)
+	if err != nil {
+		return nil, err
+	}
+	if req.GetBuilder() == "" || req.GetHash() == "" {
+		return nil, grpcstatus.Error(codes.InvalidArgument, "Builder and Hash must be provided")
+	}
+	if err := g.clearFromDashboard(ctx, req.GetBuilder(), req.GetHash(), key); err != nil {
+		return nil, err
+	}
+	return &protos.ClearResultsResponse{}, nil
+}
+
+// clearFromDashboard calls the dashboard API to remove a build.
+// TODO(golang.org/issue/34744) - Remove after switching to wiping in the Coordinator.
+func (g *gRPCServer) clearFromDashboard(ctx context.Context, builder, hash, key string) error {
+	u, err := url.Parse(g.dashboardURL)
+	if err != nil {
+		log.Printf("gRPCServer.ClearResults: Error parsing dashboardURL %q: %v", g.dashboardURL, err)
+		return grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	u.Path = "/clear-results"
+	form := url.Values{
+		"builder": {builder},
+		"hash":    {hash},
+		"key":     {key},
+	}
+	u.RawQuery = form.Encode() // The Dashboard API does not read the POST body.
+	clearReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil)
+	if err != nil {
+		log.Printf("gRPCServer.ClearResults: error creating http request: %v", err)
+		return grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	resp, err := http.DefaultClient.Do(clearReq)
+	if err != nil {
+		log.Printf("gRPCServer.ClearResults: error performing wipe for %q/%q: %v", builder, hash, err)
+		return grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		log.Printf("gRPCServer.ClearResults: error reading response body for %q/%q: %v", builder, hash, err)
+		return grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	if resp.StatusCode != http.StatusOK {
+		log.Printf("gRPCServer.ClearResults: bad status from dashboard: %v (%q)", resp.StatusCode, resp.Status)
+		code, ok := statusToCode[resp.StatusCode]
+		if !ok {
+			code = codes.Internal
+		}
+		return grpcstatus.Error(code, code.String())
+	}
+	if len(body) == 0 {
+		return nil
+	}
+	dr := new(dashboardResponse)
+	if err := json.Unmarshal(body, dr); err != nil {
+		log.Printf("gRPCServer.ClearResults: error parsing response body for %q/%q: %v", builder, hash, err)
+		return grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	if dr.Error == "datastore: concurrent transaction" {
+		return grpcstatus.Error(codes.Aborted, dr.Error)
+	}
+	if dr.Error != "" {
+		return grpcstatus.Error(codes.FailedPrecondition, dr.Error)
+	}
+	return nil
+}
+
+// dashboardResponse mimics the dashResponse struct from app/appengine.
+// TODO(golang.org/issue/34744) - Remove after switching to wiping in the Coordinator.
+type dashboardResponse struct {
+	// Error is an error string describing the API response. The dashboard API semantics are to always return a
+	// 200, and populate this field with details.
+	Error string `json:"Error"`
+	// Response a human friendly response from the API. It is not populated for build status clear responses.
+	Response string `json:"Response"`
+}
+
+// statusToCode maps HTTP status codes to gRPC codes. It purposefully only contains statuses we care to map.
+// TODO(golang.org/issue/34744) - Move to shared file or library.
+var statusToCode = map[int]codes.Code{
+	http.StatusOK:                  codes.OK,
+	http.StatusBadRequest:          codes.InvalidArgument,
+	http.StatusUnauthorized:        codes.Unauthenticated,
+	http.StatusForbidden:           codes.PermissionDenied,
+	http.StatusNotFound:            codes.NotFound,
+	http.StatusConflict:            codes.Aborted,
+	http.StatusGone:                codes.DataLoss,
+	http.StatusTooManyRequests:     codes.ResourceExhausted,
+	http.StatusInternalServerError: codes.Internal,
+	http.StatusNotImplemented:      codes.Unimplemented,
+	http.StatusServiceUnavailable:  codes.Unavailable,
+	http.StatusGatewayTimeout:      codes.DeadlineExceeded,
+}
+
+// keyFromContext loads a builder key from request metadata.
+//
+// The metadata format is prefixed with "builder " to avoid collisions with OAuth:
+//    authorization: builder MYKEY
+//
+// TODO(golang.org/issue/34744) - Move to shared file or library. This would make a nice UnaryServerInterceptor.
+// TODO(golang.org/issue/34744) - Currently allows the Build Dashboard to validate tokens, but we should validate here.
+func keyFromContext(ctx context.Context) (string, error) {
+	md, ok := metadata.FromIncomingContext(ctx)
+	if !ok {
+		return "", grpcstatus.Error(codes.Internal, codes.Internal.String())
+	}
+	auth := md.Get("authorization")
+	if len(auth) == 0 || len(auth[0]) < 9 || !strings.HasPrefix(auth[0], "builder ") {
+		return "", grpcstatus.Error(codes.Unauthenticated, codes.Unauthenticated.String())
+	}
+	key := auth[0][8:len(auth[0])]
+	return key, nil
+}
diff --git a/cmd/coordinator/results_test.go b/cmd/coordinator/results_test.go
new file mode 100644
index 0000000..cc868da
--- /dev/null
+++ b/cmd/coordinator/results_test.go
@@ -0,0 +1,166 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.13
+// +build linux darwin
+
+package main
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"golang.org/x/build/cmd/coordinator/protos"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/metadata"
+	grpcstatus "google.golang.org/grpc/status"
+)
+
+// fakeDashboard implements a fake version of the Build Dashboard API for testing.
+// TODO(golang.org/issue/34744) - Remove with build dashboard API client removal.
+type fakeDashboard struct {
+	returnBody   string
+	returnStatus int
+}
+
+func (f *fakeDashboard) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		http.Error(rw, "method must be POST", http.StatusBadRequest)
+		return
+	}
+	if r.URL.Path != "/clear-results" {
+		http.NotFound(rw, r)
+		return
+	}
+	if f.returnStatus != 0 && f.returnStatus != http.StatusOK {
+		http.Error(rw, `{"Error": "`+http.StatusText(f.returnStatus)+`"}`, f.returnStatus)
+		return
+	}
+	r.ParseForm()
+	if r.FormValue("builder") == "" || r.FormValue("hash") == "" || r.FormValue("key") == "" {
+		http.Error(rw, `{"Error": "missing builder, hash, or key"}`, http.StatusBadRequest)
+		return
+	}
+	if f.returnBody == "" {
+		rw.Write([]byte("{}"))
+		return
+	}
+	rw.Write([]byte(f.returnBody))
+	return
+}
+
+func TestClearResults(t *testing.T) {
+	req := &protos.ClearResultsRequest{Builder: "somebuilder", Hash: "somehash"}
+	fd := new(fakeDashboard)
+	s := httptest.NewServer(fd)
+	defer s.Close()
+
+	md := metadata.New(map[string]string{"authorization": "builder mykey"})
+	ctx := metadata.NewIncomingContext(context.Background(), md)
+	gs := &gRPCServer{dashboardURL: s.URL}
+	_, err := gs.ClearResults(ctx, req)
+	if err != nil {
+		t.Errorf("cli.ClearResults(%v, %v) = _, %v, wanted no error", ctx, req, err)
+	}
+
+	if grpcstatus.Code(err) != codes.OK {
+		t.Errorf("cli.ClearResults(%v, %v) = _, %v, wanted %v", ctx, req, err, codes.OK)
+	}
+}
+
+func TestClearResultsErrors(t *testing.T) {
+	cases := []struct {
+		desc     string
+		key      string
+		req      *protos.ClearResultsRequest
+		apiCode  int
+		apiResp  string
+		wantCode codes.Code
+	}{
+		{
+			desc: "missing key",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+				Hash:    "ABCDEF1234567890",
+			},
+			wantCode: codes.Unauthenticated,
+		},
+		{
+			desc: "missing builder",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Hash: "ABCDEF1234567890",
+			},
+			wantCode: codes.InvalidArgument,
+		},
+		{
+			desc: "missing hash",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+			},
+			wantCode: codes.InvalidArgument,
+		},
+		{
+			desc: "dashboard API error",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+				Hash:    "ABCDEF1234567890",
+			},
+			apiCode:  http.StatusBadRequest,
+			wantCode: codes.InvalidArgument,
+		},
+		{
+			desc: "dashboard API unknown status",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+				Hash:    "ABCDEF1234567890",
+			},
+			apiCode:  http.StatusPermanentRedirect,
+			wantCode: codes.Internal,
+		},
+		{
+			desc: "dashboard API retryable error",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+				Hash:    "ABCDEF1234567890",
+			},
+			apiCode:  http.StatusOK,
+			apiResp:  `{"Error": "datastore: concurrent transaction"}`,
+			wantCode: codes.Aborted,
+		},
+		{
+			desc: "dashboard API other error",
+			key:  "somekey",
+			req: &protos.ClearResultsRequest{
+				Builder: "local",
+				Hash:    "ABCDEF1234567890",
+			},
+			apiCode:  http.StatusOK,
+			apiResp:  `{"Error": "no matching builder found"}`,
+			wantCode: codes.FailedPrecondition,
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.desc, func(t *testing.T) {
+			fd := &fakeDashboard{returnStatus: c.apiCode, returnBody: c.apiResp}
+			s := httptest.NewServer(fd)
+			defer s.Close()
+
+			md := metadata.New(map[string]string{"authorization": "builder " + c.key})
+			ctx := metadata.NewIncomingContext(context.Background(), md)
+			gs := &gRPCServer{dashboardURL: s.URL}
+			_, err := gs.ClearResults(ctx, c.req)
+
+			if grpcstatus.Code(err) != c.wantCode {
+				t.Errorf("cli.ClearResults(%v, %v) = _, %v, wanted %v", ctx, c.req, err, c.wantCode)
+			}
+		})
+	}
+}
diff --git a/cmd/coordinator/status.go b/cmd/coordinator/status.go
index 09ac89f..ab05732 100644
--- a/cmd/coordinator/status.go
+++ b/cmd/coordinator/status.go
@@ -556,6 +556,12 @@
 func uptime() time.Duration { return time.Since(processStartTime).Round(time.Second) }
 
 func handleStatus(w http.ResponseWriter, r *http.Request) {
+	// Support gRPC handlers. handleStatus is our toplevel ("/") handler, so reroute to the gRPC server for
+	// matching requests.
+	if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
+		grpcServer.ServeHTTP(w, r)
+		return
+	}
 	if r.URL.Path != "/" {
 		http.NotFound(w, r)
 		return
diff --git a/cmd/retrybuilds/retrybuilds.go b/cmd/retrybuilds/retrybuilds.go
index 51a22da..3f84f89 100644
--- a/cmd/retrybuilds/retrybuilds.go
+++ b/cmd/retrybuilds/retrybuilds.go
@@ -18,8 +18,10 @@
 
 import (
 	"bytes"
+	"context"
 	"crypto/hmac"
 	"crypto/md5"
+	"crypto/tls"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -32,6 +34,13 @@
 	"strings"
 	"sync"
 	"time"
+
+	"golang.org/x/build/cmd/coordinator/protos"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/status"
 )
 
 var (
@@ -45,6 +54,8 @@
 	sendMasterKey = flag.Bool("sendmaster", false, "send the master key in request instead of a builder-specific key; allows overriding actions of revoked keys")
 	branch        = flag.String("branch", "master", "branch to find flakes from (for use with -redo-flaky)")
 	substr        = flag.String("substr", "", "if non-empty, redoes all build failures whose failure logs contain this substring")
+	// TODO(golang.org/issue/34744) - remove after gRPC API for ClearResults is deployed
+	grpcHost = flag.String("grpc-host", "", "(EXPERIMENTAL) use gRPC for communicating with the API.")
 )
 
 type Failure struct {
@@ -56,11 +67,20 @@
 func main() {
 	flag.Parse()
 	*builderPrefix = strings.TrimSuffix(*builderPrefix, "/")
+	cl := client{}
+	if *grpcHost != "" {
+		tc := &tls.Config{InsecureSkipVerify: strings.HasPrefix(*grpcHost, "localhost:")}
+		cc, err := grpc.DialContext(context.Background(), *grpcHost, grpc.WithTransportCredentials(credentials.NewTLS(tc)))
+		if err != nil {
+			log.Fatalf("grpc.DialContext(_, %q, _) = %v, wanted no error", *grpcHost, err)
+		}
+		cl.coordinator = protos.NewCoordinatorClient(cc)
+	}
 	if *logHash != "" {
 		substr := "/log/" + *logHash
 		for _, f := range failures() {
 			if strings.Contains(f.LogURL, substr) {
-				wipe(f.Builder, f.Hash)
+				cl.wipe(f.Builder, f.Hash)
 			}
 		}
 		return
@@ -69,7 +89,7 @@
 		foreachFailure(func(f Failure, failLog string) {
 			if strings.Contains(failLog, *substr) {
 				log.Printf("Restarting %+v", f)
-				wipe(f.Builder, f.Hash)
+				cl.wipe(f.Builder, f.Hash)
 			}
 		})
 		return
@@ -78,7 +98,7 @@
 		foreachFailure(func(f Failure, failLog string) {
 			if isFlaky(failLog) {
 				log.Printf("Restarting flaky %+v", f)
-				wipe(f.Builder, f.Hash)
+				cl.wipe(f.Builder, f.Hash)
 			}
 		})
 		return
@@ -91,11 +111,11 @@
 			if f.Builder != *builder {
 				continue
 			}
-			wipe(f.Builder, f.Hash)
+			cl.wipe(f.Builder, f.Hash)
 		}
 		return
 	}
-	wipe(*builder, fullHash(*hash))
+	cl.wipe(*builder, fullHash(*hash))
 }
 
 func foreachFailure(fn func(f Failure, failLog string)) {
@@ -198,9 +218,49 @@
 	panic("unreachable")
 }
 
+type client struct {
+	coordinator protos.CoordinatorClient
+}
+
+// grpcWipe wipes a git hash failure for the provided builder and hash.
+// Only the main Go repo is currently supported.
+// TODO(golang.org/issue/34744) - replace HTTP wipe with this after gRPC API for ClearResults is deployed
+func (c *client) grpcWipe(builder, hash string) {
+	md := metadata.New(map[string]string{"authorization": "builder " + builderKey(builder)})
+	for i := 0; i < 10; i++ {
+		ctx, cancel := context.WithTimeout(metadata.NewOutgoingContext(context.Background(), md), time.Minute)
+		resp, err := c.coordinator.ClearResults(ctx, &protos.ClearResultsRequest{
+			Builder: builder,
+			Hash:    hash,
+		})
+		cancel()
+		if err != nil {
+			s, _ := status.FromError(err)
+			switch s.Code() {
+			case codes.Aborted:
+				log.Printf("Concurrent datastore transaction wiping %v %v: retrying in 1 second", builder, hash)
+				time.Sleep(time.Second)
+			case codes.DeadlineExceeded:
+				log.Printf("Timeout wiping %v %v: retrying", builder, hash)
+			default:
+				log.Fatalln(err)
+			}
+			continue
+		}
+		log.Printf("cl.ClearResults(%q, %q) = %v: resp: %v", builder, hash, status.Code(err), resp)
+		return
+	}
+}
+
 // wipe wipes the git hash failure for the provided failure.
 // Only the main go repo is currently supported.
-func wipe(builder, hash string) {
+func (c *client) wipe(builder, hash string) {
+	if *grpcHost != "" {
+		// TODO(golang.org/issue/34744) - Remove HTTP logic after gRPC API for ClearResults is deployed
+		// to the Coordinator.
+		c.grpcWipe(builder, hash)
+		return
+	}
 	vals := url.Values{
 		"builder": {builder},
 		"hash":    {hash},
diff --git a/go.mod b/go.mod
index 9c88aa2..c866c55 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@
 	github.com/davecgh/go-spew v1.1.1
 	github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
 	github.com/gliderlabs/ssh v0.1.1
-	github.com/golang/protobuf v1.3.2
+	github.com/golang/protobuf v1.3.3
 	github.com/google/go-cmp v0.4.0
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-querystring v1.0.0 // indirect
@@ -25,16 +25,16 @@
 	github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
 	go4.org v0.0.0-20180809161055-417644f6feb5
 	golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
-	golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
+	golang.org/x/net v0.0.0-20200202094626-16171245cfb2
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852
 	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
-	golang.org/x/sys v0.0.0-20200113162924-86b910548bc1
+	golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5
 	golang.org/x/text v0.3.2
 	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
-	google.golang.org/api v0.15.0
-	google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b
-	google.golang.org/grpc v1.26.0
+	google.golang.org/api v0.17.0
+	google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6
+	google.golang.org/grpc v1.27.1
 	gopkg.in/inf.v0 v0.9.1
 	grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
 )
diff --git a/go.sum b/go.sum
index 3ec0e58..725aea6 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,7 @@
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -57,13 +58,18 @@
 github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
@@ -105,6 +111,7 @@
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
@@ -125,6 +132,8 @@
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -140,6 +149,8 @@
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
@@ -158,13 +169,18 @@
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -187,11 +203,14 @@
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1 h1:gZpLHxUX5BdYLA08Lj4YCJNN/jk7KtquiArPoeX0WvA=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@@ -207,6 +226,8 @@
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -221,12 +242,15 @@
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
 google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
 google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
 google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -239,17 +263,24 @@
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b h1:c8OBoXP3kTbDWWB/oVE3FkR851p4iZ3MPadz7zXEIPU=
-google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6 h1:tirixpud1WdjE3/NrL9ar4ot0ADfwls8sOcIf1ivRDw=
+google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -263,6 +294,8 @@
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=