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  }