golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/results.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 // Code related to the Build Results API. 8 9 package main 10 11 import ( 12 "context" 13 "encoding/json" 14 "io" 15 "log" 16 "net/http" 17 "net/url" 18 "strings" 19 20 "golang.org/x/build/cmd/coordinator/protos" 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/metadata" 23 grpcstatus "google.golang.org/grpc/status" 24 ) 25 26 type gRPCServer struct { 27 // embed an UnimplementedCoordinatorServer to avoid errors when adding new RPCs to the proto. 28 *protos.UnimplementedCoordinatorServer 29 30 // dashboardURL is the base URL of the Dashboard service (https://build.golang.org) 31 dashboardURL string 32 } 33 34 // ClearResults implements the ClearResults RPC call from the CoordinatorService. 35 // 36 // It currently hits the build Dashboard service to clear a result. 37 // TODO(golang.org/issue/34744) - Change to wipe build status from the Coordinator itself after findWork 38 // starts using maintner. 39 func (g *gRPCServer) ClearResults(ctx context.Context, req *protos.ClearResultsRequest) (*protos.ClearResultsResponse, error) { 40 key, err := keyFromContext(ctx) 41 if err != nil { 42 return nil, err 43 } 44 if req.GetBuilder() == "" || req.GetHash() == "" { 45 return nil, grpcstatus.Error(codes.InvalidArgument, "Builder and Hash must be provided") 46 } 47 if err := g.clearFromDashboard(ctx, req.GetBuilder(), req.GetHash(), key); err != nil { 48 return nil, err 49 } 50 return &protos.ClearResultsResponse{}, nil 51 } 52 53 // clearFromDashboard calls the dashboard API to remove a build. 54 // TODO(golang.org/issue/34744) - Remove after switching to wiping in the Coordinator. 55 func (g *gRPCServer) clearFromDashboard(ctx context.Context, builder, hash, key string) error { 56 u, err := url.Parse(g.dashboardURL) 57 if err != nil { 58 log.Printf("gRPCServer.ClearResults: Error parsing dashboardURL %q: %v", g.dashboardURL, err) 59 return grpcstatus.Error(codes.Internal, codes.Internal.String()) 60 } 61 u.Path = "/clear-results" 62 form := url.Values{ 63 "builder": {builder}, 64 "hash": {hash}, 65 "key": {key}, 66 } 67 u.RawQuery = form.Encode() // The Dashboard API does not read the POST body. 68 clearReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil) 69 if err != nil { 70 log.Printf("gRPCServer.ClearResults: error creating http request: %v", err) 71 return grpcstatus.Error(codes.Internal, codes.Internal.String()) 72 } 73 resp, err := http.DefaultClient.Do(clearReq) 74 if err != nil { 75 log.Printf("gRPCServer.ClearResults: error performing wipe for %q/%q: %v", builder, hash, err) 76 return grpcstatus.Error(codes.Internal, codes.Internal.String()) 77 } 78 body, err := io.ReadAll(resp.Body) 79 resp.Body.Close() 80 if err != nil { 81 log.Printf("gRPCServer.ClearResults: error reading response body for %q/%q: %v", builder, hash, err) 82 return grpcstatus.Error(codes.Internal, codes.Internal.String()) 83 } 84 if resp.StatusCode != http.StatusOK { 85 log.Printf("gRPCServer.ClearResults: bad status from dashboard: %v (%q)", resp.StatusCode, resp.Status) 86 code, ok := statusToCode[resp.StatusCode] 87 if !ok { 88 code = codes.Internal 89 } 90 return grpcstatus.Error(code, code.String()) 91 } 92 if len(body) == 0 { 93 return nil 94 } 95 dr := new(dashboardResponse) 96 if err := json.Unmarshal(body, dr); err != nil { 97 log.Printf("gRPCServer.ClearResults: error parsing response body for %q/%q: %v", builder, hash, err) 98 return grpcstatus.Error(codes.Internal, codes.Internal.String()) 99 } 100 if dr.Error == "datastore: concurrent transaction" { 101 return grpcstatus.Error(codes.Aborted, dr.Error) 102 } 103 if dr.Error != "" { 104 return grpcstatus.Error(codes.FailedPrecondition, dr.Error) 105 } 106 return nil 107 } 108 109 // dashboardResponse mimics the dashResponse struct from app/appengine. 110 // TODO(golang.org/issue/34744) - Remove after switching to wiping in the Coordinator. 111 type dashboardResponse struct { 112 // Error is an error string describing the API response. The dashboard API semantics are to always return a 113 // 200, and populate this field with details. 114 Error string `json:"Error"` 115 // Response a human friendly response from the API. It is not populated for build status clear responses. 116 Response string `json:"Response"` 117 } 118 119 // statusToCode maps HTTP status codes to gRPC codes. It purposefully only contains statuses we care to map. 120 // TODO(golang.org/issue/34744) - Move to shared file or library. 121 var statusToCode = map[int]codes.Code{ 122 http.StatusOK: codes.OK, 123 http.StatusBadRequest: codes.InvalidArgument, 124 http.StatusUnauthorized: codes.Unauthenticated, 125 http.StatusForbidden: codes.PermissionDenied, 126 http.StatusNotFound: codes.NotFound, 127 http.StatusConflict: codes.Aborted, 128 http.StatusGone: codes.DataLoss, 129 http.StatusTooManyRequests: codes.ResourceExhausted, 130 http.StatusInternalServerError: codes.Internal, 131 http.StatusNotImplemented: codes.Unimplemented, 132 http.StatusServiceUnavailable: codes.Unavailable, 133 http.StatusGatewayTimeout: codes.DeadlineExceeded, 134 } 135 136 // keyFromContext loads a builder key from request metadata. 137 // 138 // TODO(golang.org/issue/34744) - Move to shared file or library. This would make a nice UnaryServerInterceptor. 139 // TODO(golang.org/issue/34744) - Currently allows the Build Dashboard to validate tokens, but we should validate here. 140 func keyFromContext(ctx context.Context) (string, error) { 141 md, ok := metadata.FromIncomingContext(ctx) 142 if !ok { 143 return "", grpcstatus.Error(codes.Internal, codes.Internal.String()) 144 } 145 auth := md.Get("coordinator-authorization") 146 if len(auth) == 0 || len(auth[0]) < 9 || !strings.HasPrefix(auth[0], "builder ") { 147 return "", grpcstatus.Error(codes.Unauthenticated, codes.Unauthenticated.String()) 148 } 149 key := auth[0][8:len(auth[0])] 150 return key, nil 151 }