forked from golang/build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
results.go
156 lines (144 loc) · 5.86 KB
/
results.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 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.
//go:build go1.13 && (linux || darwin)
// +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
}