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  }