golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/sign/server.go (about)

     1  // Copyright 2022 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  package sign
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"log"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"golang.org/x/build/internal/access"
    16  	"golang.org/x/build/internal/relui/protos"
    17  	"golang.org/x/sync/errgroup"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  )
    21  
    22  var _ Service = (*SigningServer)(nil)
    23  
    24  // SigningServer is a GRPC signing server used to send signing requests and
    25  // signing status requests to a client.
    26  type SigningServer struct {
    27  	protos.UnimplementedReleaseServiceServer
    28  
    29  	// requests is a channel of outgoing signing requests to be sent to
    30  	// any of the connected signing clients.
    31  	requests chan *protos.SigningRequest
    32  
    33  	// callback is a map of the message ID to callback association used to
    34  	// respond to previously created requests to a channel.
    35  	callbackMu sync.Mutex
    36  	callback   map[string]func(*signResponse) // Key is message ID.
    37  }
    38  
    39  // NewServer creates a GRPC signing server used to send signing requests and
    40  // signing status requests to a client.
    41  func NewServer() *SigningServer {
    42  	return &SigningServer{
    43  		requests: make(chan *protos.SigningRequest),
    44  		callback: make(map[string]func(*signResponse)),
    45  	}
    46  }
    47  
    48  // UpdateSigningStatus uses a bidirectional streaming connection to send signing requests to the client
    49  // and receive status updates on signing requests. There is no specific order which the requests or responses
    50  // need to occur in. The connection returns an error once the context is canceled or an error is encountered.
    51  func (rs *SigningServer) UpdateSigningStatus(stream protos.ReleaseService_UpdateSigningStatusServer) error {
    52  	iap, err := access.IAPFromContext(stream.Context())
    53  	if err != nil {
    54  		return status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
    55  	}
    56  
    57  	t := time.Now()
    58  	log.Printf("SigningServer: a client connected (iap = %+v)\n", iap)
    59  	g, ctx := errgroup.WithContext(stream.Context())
    60  	g.Go(func() error {
    61  		for {
    62  			select {
    63  			case <-ctx.Done():
    64  				return ctx.Err()
    65  			case req := <-rs.requests:
    66  				err := stream.Send(req)
    67  				if err != nil {
    68  					rs.callAndDeregister(req.GetMessageId(), &signResponse{err: err})
    69  					return err
    70  				}
    71  			}
    72  		}
    73  	})
    74  	g.Go(func() error {
    75  		for {
    76  			resp, err := stream.Recv()
    77  			if err != nil {
    78  				return err
    79  			}
    80  			rs.callAndDeregister(resp.GetMessageId(), &signResponse{status: resp})
    81  		}
    82  	})
    83  	err = g.Wait()
    84  	log.Printf("SigningServer: a client disconnected after %v (err = %v)\n", time.Since(t), err)
    85  	return err
    86  }
    87  
    88  // do sends a signing request and returns the corresponding signing response.
    89  // It blocks until a response is received or the context times out or is canceled.
    90  func (rs *SigningServer) do(ctx context.Context, req *protos.SigningRequest) (resp *protos.SigningStatus, err error) {
    91  	t := time.Now()
    92  	defer func() {
    93  		if err == nil {
    94  			log.Printf("SigningServer: successfully round-tripped message=%q in %v:\n  req = %v\n  resp = %v\n", req.GetMessageId(), time.Since(t), req, resp)
    95  		} else {
    96  			log.Printf("SigningServer: communication error %v for message=%q after %v:\n  req = %v\n", err, req.GetMessageId(), time.Since(t), req)
    97  		}
    98  	}()
    99  
   100  	// Register where to send the response for this message ID.
   101  	respCh := make(chan *signResponse, 1) // Room for one response.
   102  	rs.register(req.GetMessageId(), func(r *signResponse) { respCh <- r })
   103  
   104  	// Send the request.
   105  	select {
   106  	case rs.requests <- req:
   107  	case <-ctx.Done():
   108  		rs.deregister(req.GetMessageId())
   109  		return nil, ctx.Err()
   110  	}
   111  
   112  	// Wait for the response.
   113  	select {
   114  	case resp := <-respCh:
   115  		return resp.status, resp.err
   116  	case <-ctx.Done():
   117  		rs.deregister(req.GetMessageId())
   118  		return nil, ctx.Err()
   119  	}
   120  }
   121  
   122  // SignArtifact implements Service.
   123  func (rs *SigningServer) SignArtifact(ctx context.Context, bt BuildType, objectURI []string) (jobID string, _ error) {
   124  	resp, err := rs.do(ctx, &protos.SigningRequest{
   125  		MessageId: uuid.NewString(),
   126  		RequestOneof: &protos.SigningRequest_Sign{Sign: &protos.SignArtifactRequest{
   127  			BuildType: bt.proto(),
   128  			GcsUri:    objectURI,
   129  		}},
   130  	})
   131  	if err != nil {
   132  		return "", err
   133  	}
   134  	switch t := resp.StatusOneof.(type) {
   135  	case *protos.SigningStatus_Started:
   136  		return t.Started.JobId, nil
   137  	case *protos.SigningStatus_Failed:
   138  		return "", fmt.Errorf("failed to start %v signing on %q: %s", bt, objectURI, t.Failed.GetDescription())
   139  	default:
   140  		return "", fmt.Errorf("unexpected response type %T for a sign request", t)
   141  	}
   142  }
   143  
   144  // ArtifactSigningStatus implements Service.
   145  func (rs *SigningServer) ArtifactSigningStatus(ctx context.Context, jobID string) (_ Status, desc string, objectURI []string, _ error) {
   146  	resp, err := rs.do(ctx, &protos.SigningRequest{
   147  		MessageId: uuid.NewString(),
   148  		RequestOneof: &protos.SigningRequest_Status{Status: &protos.SignArtifactStatusRequest{
   149  			JobId: jobID,
   150  		}},
   151  	})
   152  	if err != nil {
   153  		return StatusUnknown, "", nil, err
   154  	}
   155  	switch t := resp.StatusOneof.(type) {
   156  	case *protos.SigningStatus_Completed:
   157  		return StatusCompleted, "", t.Completed.GetGcsUri(), nil
   158  	case *protos.SigningStatus_Failed:
   159  		return StatusFailed, t.Failed.GetDescription(), nil, nil
   160  	case *protos.SigningStatus_NotFound:
   161  		return StatusNotFound, fmt.Sprintf("signing job %q not found", jobID), nil, nil
   162  	case *protos.SigningStatus_Running:
   163  		return StatusRunning, t.Running.GetDescription(), nil, nil
   164  	default:
   165  		return 0, "", nil, fmt.Errorf("unexpected response type %T for a status request", t)
   166  	}
   167  }
   168  
   169  // CancelSigning implements Service.
   170  func (rs *SigningServer) CancelSigning(ctx context.Context, jobID string) error {
   171  	_, err := rs.do(ctx, &protos.SigningRequest{
   172  		MessageId: uuid.NewString(),
   173  		RequestOneof: &protos.SigningRequest_Cancel{Cancel: &protos.SignArtifactCancelRequest{
   174  			JobId: jobID,
   175  		}},
   176  	})
   177  	return err
   178  }
   179  
   180  // signResponse contains the response and error from a signing request.
   181  type signResponse struct {
   182  	status *protos.SigningStatus
   183  	err    error
   184  }
   185  
   186  // register creates a message ID to channel association.
   187  func (s *SigningServer) register(messageID string, f func(*signResponse)) {
   188  	s.callbackMu.Lock()
   189  	s.callback[messageID] = f
   190  	s.callbackMu.Unlock()
   191  }
   192  
   193  // deregister removes the channel to message ID association.
   194  func (s *SigningServer) deregister(messageID string) {
   195  	s.callbackMu.Lock()
   196  	delete(s.callback, messageID)
   197  	s.callbackMu.Unlock()
   198  }
   199  
   200  // callAndDeregister calls the callback associated with the message ID.
   201  // If no callback is registered for the message ID, the response is dropped.
   202  // The callback registration is always removed if it exists.
   203  func (s *SigningServer) callAndDeregister(messageID string, resp *signResponse) {
   204  	s.callbackMu.Lock()
   205  	defer s.callbackMu.Unlock()
   206  
   207  	respFunc, ok := s.callback[messageID]
   208  	if !ok {
   209  		// drop the message
   210  		log.Printf("SigningServer: caller not found for message=%q", messageID)
   211  		return
   212  	}
   213  	delete(s.callback, messageID)
   214  	respFunc(resp)
   215  }