go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/artifactcontent/rbecas.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package artifactcontent
    16  
    17  import (
    18  	"context"
    19  	"flag"
    20  	"fmt"
    21  	"io"
    22  	"strings"
    23  
    24  	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    25  	"go.opentelemetry.io/otel/attribute"
    26  	"golang.org/x/sync/errgroup"
    27  	"google.golang.org/genproto/googleapis/bytestream"
    28  	"google.golang.org/grpc"
    29  	"google.golang.org/grpc/codes"
    30  	"google.golang.org/grpc/credentials"
    31  	"google.golang.org/grpc/status"
    32  
    33  	"go.chromium.org/luci/common/errors"
    34  	"go.chromium.org/luci/common/logging"
    35  	"go.chromium.org/luci/grpc/appstatus"
    36  	"go.chromium.org/luci/grpc/grpcmon"
    37  	"go.chromium.org/luci/resultdb/internal/tracing"
    38  	"go.chromium.org/luci/server/auth"
    39  	"go.chromium.org/luci/server/router"
    40  )
    41  
    42  // RBEConn creates a gRPC connection to RBE authenticated as self.
    43  func RBEConn(ctx context.Context) (*grpc.ClientConn, error) {
    44  	creds, err := auth.GetPerRPCCredentials(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...))
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	return grpc.Dial(
    50  		"remotebuildexecution.googleapis.com:443",
    51  		grpc.WithTransportCredentials(credentials.NewTLS(nil)),
    52  		grpc.WithPerRPCCredentials(creds),
    53  		grpc.WithStatsHandler(&grpcmon.ClientRPCStatsMonitor{}),
    54  		grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    55  		grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    56  	)
    57  }
    58  
    59  // RegisterRBEInstanceFlag registers -artifact-rbe-instance flag.
    60  func RegisterRBEInstanceFlag(fs *flag.FlagSet, target *string) {
    61  	fs.StringVar(
    62  		target,
    63  		"artifact-rbe-instance",
    64  		"",
    65  		"Name of the RBE instance to use for artifact storage",
    66  	)
    67  }
    68  
    69  // handleRBECASContent serves artifact content stored in RBE-CAS.
    70  func (r *contentRequest) handleRBECASContent(c *router.Context, hash string) {
    71  	// Protocol:
    72  	// https://github.com/bazelbuild/remote-apis/blob/7802003e00901b4e740fe0ebec1243c221e02ae2/build/bazel/remote/execution/v2/remote_execution.proto#L229-L233
    73  	// https://github.com/googleapis/googleapis/blob/c8e291e6a4d60771219205b653715d5aeec3e96b/google/bytestream/bytestream.proto#L50-L53
    74  
    75  	// Start a reading stream.
    76  	stream, err := r.ReadCASBlob(c.Request.Context(), &bytestream.ReadRequest{
    77  		ResourceName: resourceName(r.RBECASInstanceName, hash, r.size.Int64),
    78  		ReadLimit:    r.limit,
    79  	})
    80  	if err != nil {
    81  		if status.Code(err) == codes.NotFound {
    82  			// Do not lose the original error message.
    83  			logging.Warningf(c.Request.Context(), "RBE-CAS responded: %s", err)
    84  			err = appstatus.Errorf(codes.NotFound, "artifact content no longer exists")
    85  		}
    86  		r.sendError(c.Request.Context(), err)
    87  		return
    88  	}
    89  
    90  	// Forward the blob to the client.
    91  	wroteHeader := false
    92  	for {
    93  		_, readSpan := tracing.Start(c.Request.Context(), "resultdb.readChunk")
    94  		chunk, err := stream.Recv()
    95  		if err == nil {
    96  			readSpan.SetAttributes(attribute.Int("size", len(chunk.Data)))
    97  		}
    98  		tracing.End(readSpan, err)
    99  
   100  		switch {
   101  		case err == io.EOF:
   102  			// We are done.
   103  			return
   104  
   105  		case err != nil:
   106  			if wroteHeader {
   107  				// The response was already partially written, so it is too late to
   108  				// write headers. Write at least something indicating the incomplete
   109  				// response.
   110  				fmt.Fprintf(c.Writer, "\nResultDB: internal error while writing the response!\n")
   111  				logging.Errorf(c.Request.Context(), "Failed to read from RBE-CAS in the middle of response: %s", err)
   112  			} else {
   113  				if status.Code(err) == codes.NotFound {
   114  					// Sometimes RBE-CAS doesn't report NotFound until the read
   115  					// of the first chunk, so duplicate the NotFound handling
   116  					// above.
   117  					// Do not lose the original error message.
   118  					logging.Warningf(c.Request.Context(), "RBE-CAS responded: %s", err)
   119  					err = appstatus.Errorf(codes.NotFound, "artifact content no longer exists")
   120  				}
   121  				r.sendError(c.Request.Context(), err)
   122  			}
   123  			return
   124  
   125  		default:
   126  			// Forward the chunk.
   127  			if !wroteHeader {
   128  				r.writeContentHeaders()
   129  				wroteHeader = true
   130  			}
   131  
   132  			_, writeSpan := tracing.Start(c.Request.Context(), "resultdb.writeChunk",
   133  				attribute.Int("size", len(chunk.Data)),
   134  			)
   135  			_, err := c.Writer.Write(chunk.Data)
   136  			tracing.End(writeSpan, err)
   137  			if err != nil {
   138  				logging.Warningf(c.Request.Context(), "Failed to write a response chunk: %s", err)
   139  				return
   140  			}
   141  		}
   142  	}
   143  }
   144  
   145  func resourceName(instance, hash string, size int64) string {
   146  	return fmt.Sprintf("%s/blobs/%s/%d", instance, strings.TrimPrefix(hash, "sha256:"), size)
   147  }
   148  
   149  // Reader reads the artifact content from RBE-CAS.
   150  type Reader struct {
   151  	// RBEInstance is the name of the RBE instance where the artifact is stored.
   152  	// Example: "projects/luci-resultdb/instances/artifacts".
   153  	RBEInstance string
   154  	// Hash is the hash of the artifact content stored in RBE-CAS.
   155  	Hash string
   156  	// Size is the content size in bytes.
   157  	Size int64
   158  }
   159  
   160  // DownloadRBECASContent calls f for the downloaded artifact content.
   161  func (r *Reader) DownloadRBECASContent(ctx context.Context, bs bytestream.ByteStreamClient, f func(context.Context, io.Reader) error) error {
   162  	stream, err := bs.Read(ctx, &bytestream.ReadRequest{
   163  		ResourceName: resourceName(r.RBEInstance, r.Hash, r.Size),
   164  	})
   165  	if err != nil {
   166  		if status.Code(err) == codes.NotFound {
   167  			logging.Warningf(ctx, "RBE-CAS responded: %s", err)
   168  		}
   169  		return err
   170  	}
   171  
   172  	pr, pw := io.Pipe()
   173  	eg, ctx := errgroup.WithContext(ctx)
   174  	defer eg.Wait()
   175  	eg.Go(func() error {
   176  		defer pr.Close()
   177  		return f(ctx, pr)
   178  	})
   179  
   180  	eg.Go(func() error {
   181  		defer pw.Close()
   182  		for {
   183  			_, readSpan := tracing.Start(ctx, "resultdb.readChunk")
   184  			chunk, err := stream.Recv()
   185  			if err == nil {
   186  				readSpan.SetAttributes(attribute.Int("size", len(chunk.Data)))
   187  			}
   188  			tracing.End(readSpan, err)
   189  
   190  			switch {
   191  			case err == io.EOF:
   192  				// We are done.
   193  				return nil
   194  
   195  			case err != nil:
   196  				return err
   197  
   198  			default:
   199  				if _, err := pw.Write(chunk.Data); err != nil {
   200  					if err == io.ErrClosedPipe {
   201  						// If f() exits early, return nil here to see what error that f() returns.
   202  						return nil
   203  					}
   204  					return errors.Annotate(err, "write to pipe").Err()
   205  				}
   206  			}
   207  		}
   208  	})
   209  
   210  	return eg.Wait()
   211  }