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>
This commit is contained in:
Alexander Rakoczy 2020-02-11 14:20:59 -05:00
Родитель 3693b7b9d8
Коммит b077d0cce9
10 изменённых файлов: 687 добавлений и 17 удалений

Просмотреть файл

@ -45,7 +45,9 @@ import (
"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 listenAndServeTLS() {
}
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 @@ func (fn loggerFunc) CreateSpan(event string, optText ...string) spanlog.Span {
// 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 @@ func main() {
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)

Просмотреть файл

@ -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",
}

Просмотреть файл

@ -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 {}

Просмотреть файл

@ -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

155
cmd/coordinator/results.go Normal file
Просмотреть файл

@ -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
}

Просмотреть файл

@ -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)
}
})
}
}

Просмотреть файл

@ -556,6 +556,12 @@ func healthCheckerHandler(hc *healthChecker) http.Handler {
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

Просмотреть файл

@ -18,8 +18,10 @@ package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
@ -32,6 +34,13 @@ import (
"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 @@ var (
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 @@ type Failure struct {
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 @@ func main() {
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 @@ func main() {
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 @@ func main() {
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 @@ func fullHash(h string) string {
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},

12
go.mod
Просмотреть файл

@ -14,7 +14,7 @@ require (
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 @@ require (
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
)

37
go.sum
Просмотреть файл

@ -27,6 +27,7 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU
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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
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/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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 h1:iMGN4xG0cnqj3t+zOM8wUB
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
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-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
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 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
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=