go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/appengine/coordinator/flex/services.go (about) 1 // Copyright 2015 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 flex 16 17 import ( 18 "context" 19 "time" 20 21 gcst "cloud.google.com/go/storage" 22 23 "go.chromium.org/luci/common/clock" 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/gcloud/gs" 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/grpc/grpcutil" 28 "go.chromium.org/luci/server/auth" 29 30 "go.chromium.org/luci/logdog/appengine/coordinator" 31 "go.chromium.org/luci/logdog/common/storage" 32 "go.chromium.org/luci/logdog/common/storage/archive" 33 "go.chromium.org/luci/logdog/common/storage/bigtable" 34 ) 35 36 const ( 37 // maxSignedURLLifetime is the maximum allowed signed URL lifetime. 38 maxSignedURLLifetime = 1 * time.Hour 39 ) 40 41 // Services is a set of support services used by Coordinator endpoints. 42 // 43 // Each instance is valid for a single request, but can be re-used throughout 44 // that request. This is advised, as the Services instance may optionally cache 45 // values. 46 // 47 // Services methods are goroutine-safe. 48 type Services interface { 49 // Storage returns a Storage instance for the supplied log stream. 50 // 51 // The caller must close the returned instance if successful. 52 StorageForStream(ctx context.Context, state *coordinator.LogStreamState, project string) (coordinator.SigningStorage, error) 53 } 54 55 // GlobalServices is an application singleton that stores cross-request service 56 // structures. 57 // 58 // It lives in the root context. 59 type GlobalServices struct { 60 btStorage *bigtable.Storage 61 gsClientFactory func(ctx context.Context, project string) (gs.Client, error) 62 storageCache *StorageCache 63 } 64 65 // NewGlobalServices instantiates a new GlobalServices instance. 66 // 67 // Receives the location of the BigTable with intermediate logs. 68 // 69 // The Context passed to GlobalServices should be a global server Context not a 70 // request-specific Context. 71 func NewGlobalServices(ctx context.Context, bt *bigtable.Flags) (*GlobalServices, error) { 72 // LRU in-memory cache in front of BigTable. 73 storageCache := &StorageCache{} 74 75 // Construct the storage, inject the caching implementation into it. 76 storage, err := bigtable.StorageFromFlags(ctx, bt) 77 if err != nil { 78 return nil, errors.Annotate(err, "failed to connect to BigTable").Err() 79 } 80 storage.Cache = storageCache 81 82 return &GlobalServices{ 83 btStorage: storage, 84 storageCache: storageCache, 85 gsClientFactory: func(ctx context.Context, project string) (client gs.Client, e error) { 86 // TODO(vadimsh): Switch to AsProject + WithProject(project) once 87 // we are ready to roll out project scoped service accounts in Logdog. 88 transport, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 89 if err != nil { 90 return nil, errors.Annotate(err, "failed to create Google Storage RPC transport").Err() 91 } 92 prodClient, err := gs.NewProdClient(ctx, transport) 93 if err != nil { 94 return nil, errors.Annotate(err, "Failed to create GS client.").Err() 95 } 96 return prodClient, nil 97 }, 98 }, nil 99 } 100 101 // Storage returns a Storage instance for the supplied log stream. 102 // 103 // The caller must close the returned instance if successful. 104 func (gsvc *GlobalServices) StorageForStream(ctx context.Context, lst *coordinator.LogStreamState, project string) ( 105 coordinator.SigningStorage, error) { 106 107 if !lst.ArchivalState().Archived() { 108 logging.Debugf(ctx, "Log is not archived. Fetching from intermediate storage.") 109 return noSignedURLStorage{gsvc.btStorage}, nil 110 } 111 112 // Some very old logs have malformed data where they claim to be archived but 113 // have no archive or index URLs. 114 if lst.ArchiveStreamURL == "" { 115 logging.Warningf(ctx, "Log has no archive URL") 116 return nil, errors.New("log has no archive URL", grpcutil.NotFoundTag) 117 } 118 if lst.ArchiveIndexURL == "" { 119 logging.Warningf(ctx, "Log has no index URL") 120 return nil, errors.New("log has no index URL", grpcutil.NotFoundTag) 121 } 122 123 gsClient, err := gsvc.gsClientFactory(ctx, project) 124 if err != nil { 125 logging.WithError(err).Errorf(ctx, "Failed to create Google Storage client.") 126 return nil, err 127 } 128 129 logging.Fields{ 130 "indexURL": lst.ArchiveIndexURL, 131 "streamURL": lst.ArchiveStreamURL, 132 "archiveTime": lst.ArchivedTime, 133 }.Debugf(ctx, "Log is archived. Fetching from archive storage.") 134 135 st, err := archive.New(archive.Options{ 136 Index: gs.Path(lst.ArchiveIndexURL), 137 Stream: gs.Path(lst.ArchiveStreamURL), 138 Cache: gsvc.storageCache, 139 Client: gsClient, 140 }) 141 if err != nil { 142 logging.WithError(err).Errorf(ctx, "Failed to create Google Storage storage instance.") 143 return nil, err 144 } 145 146 rv := &googleStorage{ 147 Storage: st, 148 svc: gsvc, 149 gs: gsClient, 150 stream: gs.Path(lst.ArchiveStreamURL), 151 index: gs.Path(lst.ArchiveIndexURL), 152 } 153 return rv, nil 154 } 155 156 // noSignedURLStorage is a thin wrapper around a Storage instance that cannot 157 // sign URLs. 158 type noSignedURLStorage struct { 159 storage.Storage 160 } 161 162 func (noSignedURLStorage) GetSignedURLs(context.Context, *coordinator.URLSigningRequest) ( 163 *coordinator.URLSigningResponse, error) { 164 165 return nil, nil 166 } 167 168 type googleStorage struct { 169 // Storage is the base storage.Storage instance. 170 storage.Storage 171 // svc is the services instance that created this. 172 svc *GlobalServices 173 174 // gs is the backing Google Storage client. 175 gs gs.Client 176 177 // stream is the stream's Google Storage URL. 178 stream gs.Path 179 // index is the index's Google Storage URL. 180 index gs.Path 181 } 182 183 func (si *googleStorage) Close() { 184 si.Storage.Close() 185 si.gs.Close() 186 } 187 188 func (si *googleStorage) GetSignedURLs(ctx context.Context, req *coordinator.URLSigningRequest) (*coordinator.URLSigningResponse, error) { 189 signer := auth.GetSigner(ctx) 190 info, err := signer.ServiceInfo(ctx) 191 if err != nil { 192 return nil, errors.Annotate(err, "failed to get service info").Err() 193 } 194 195 lifetime := req.Lifetime 196 switch { 197 case lifetime < 0: 198 return nil, errors.Reason("invalid signed URL lifetime: %s", lifetime).Err() 199 200 case lifetime > maxSignedURLLifetime: 201 lifetime = maxSignedURLLifetime 202 } 203 204 // Get our signing options. 205 resp := coordinator.URLSigningResponse{ 206 Expiration: clock.Now(ctx).Add(lifetime), 207 } 208 opts := gcst.SignedURLOptions{ 209 GoogleAccessID: info.ServiceAccountName, 210 SignBytes: func(b []byte) ([]byte, error) { 211 _, signedBytes, err := signer.SignBytes(ctx, b) 212 return signedBytes, err 213 }, 214 Method: "GET", 215 Expires: resp.Expiration, 216 } 217 218 doSign := func(path gs.Path) (string, error) { 219 url, err := gcst.SignedURL(path.Bucket(), path.Filename(), &opts) 220 if err != nil { 221 logging.Warningf(ctx, "failed to sign URL: bucket(%s)/filename(%s)", path.Bucket(), path.Filename()) 222 return "", errors.Annotate(err, "failed to sign URL").Err() 223 } 224 return url, nil 225 } 226 227 // Sign stream URL. 228 if req.Stream { 229 if resp.Stream, err = doSign(si.stream); err != nil { 230 return nil, errors.Annotate(err, "failed to sign stream URL").Err() 231 } 232 } 233 234 // Sign index URL. 235 if req.Index { 236 if resp.Index, err = doSign(si.index); err != nil { 237 return nil, errors.Annotate(err, "failed to sign index URL").Err() 238 } 239 } 240 241 return &resp, nil 242 }