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=