зеркало из https://github.com/golang/build.git
154 строки
5.8 KiB
Go
154 строки
5.8 KiB
Go
// 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.16 && (linux || darwin)
|
|
// +build go1.16
|
|
// +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.
|
|
//
|
|
// 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("coordinator-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
|
|
}
|