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 }