go.temporal.io/server@v1.23.0/common/archiver/gcloud/connector/client.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  //go:generate mockgen -copyright_file ../../../../LICENSE -package $GOPACKAGE -source $GOFILE -destination client_mock.go
    26  
    27  package connector
    28  
    29  import (
    30  	"bytes"
    31  	"context"
    32  	"errors"
    33  	"io"
    34  	"os"
    35  
    36  	"cloud.google.com/go/storage"
    37  	"go.uber.org/multierr"
    38  	"google.golang.org/api/iterator"
    39  
    40  	"go.temporal.io/server/common/archiver"
    41  	"go.temporal.io/server/common/config"
    42  )
    43  
    44  var (
    45  	// ErrBucketNotFound is non retryable error that is thrown when the bucket doesn't exist
    46  	ErrBucketNotFound = errors.New("bucket not found")
    47  	errObjectNotFound = errors.New("object not found")
    48  )
    49  
    50  type (
    51  	// Precondition is a function that allow you to filter a query result.
    52  	// If subject match params conditions then return true, else return false.
    53  	Precondition func(subject interface{}) bool
    54  
    55  	// Client is a wrapper around Google cloud storages client library.
    56  	Client interface {
    57  		Upload(ctx context.Context, URI archiver.URI, fileName string, file []byte) error
    58  		Get(ctx context.Context, URI archiver.URI, file string) ([]byte, error)
    59  		Query(ctx context.Context, URI archiver.URI, fileNamePrefix string) ([]string, error)
    60  		QueryWithFilters(ctx context.Context, URI archiver.URI, fileNamePrefix string, pageSize, offset int, filters []Precondition) ([]string, bool, int, error)
    61  		Exist(ctx context.Context, URI archiver.URI, fileName string) (bool, error)
    62  	}
    63  
    64  	storageWrapper struct {
    65  		client GcloudStorageClient
    66  	}
    67  )
    68  
    69  // NewClient return a Temporal gcloudstorage.Client based on default google service account creadentials (ScopeFullControl required).
    70  // Bucket must be created by Iaas scripts, in other words, this library doesn't create the required Bucket.
    71  // Optionaly you can set your credential path throught "GOOGLE_APPLICATION_CREDENTIALS" environment variable or through temporal config file.
    72  // You can find more info about "Google Setting Up Authentication for Server to Server Production Applications" under the following link
    73  // https://cloud.google.com/docs/authentication/production
    74  func NewClient(ctx context.Context, config *config.GstorageArchiver) (Client, error) {
    75  	if credentialsPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); credentialsPath != "" {
    76  		clientDelegate, err := newClientDelegateWithCredentials(ctx, credentialsPath)
    77  		return &storageWrapper{client: clientDelegate}, err
    78  	}
    79  
    80  	if config.CredentialsPath != "" {
    81  		clientDelegate, err := newClientDelegateWithCredentials(ctx, config.CredentialsPath)
    82  		return &storageWrapper{client: clientDelegate}, err
    83  	}
    84  
    85  	clientDelegate, err := newDefaultClientDelegate(ctx)
    86  	return &storageWrapper{client: clientDelegate}, err
    87  
    88  }
    89  
    90  // NewClientWithParams return a gcloudstorage.Client based on input parameters
    91  func NewClientWithParams(clientD GcloudStorageClient) (Client, error) {
    92  	return &storageWrapper{client: clientD}, nil
    93  }
    94  
    95  // Upload push a file to gcloud storage bucket (sinkPath)
    96  // example:
    97  // Upload(ctx, mockBucketHandleClient, "gs://my-bucket-cad/temporal_archival/development", "45273645-fileName.history", fileReader)
    98  func (s *storageWrapper) Upload(ctx context.Context, URI archiver.URI, fileName string, file []byte) (err error) {
    99  	bucket := s.client.Bucket(URI.Hostname())
   100  	writer := bucket.Object(formatSinkPath(URI.Path()) + "/" + fileName).NewWriter(ctx)
   101  	_, err = io.Copy(writer, bytes.NewReader(file))
   102  	if err == nil {
   103  		err = writer.Close()
   104  	}
   105  
   106  	return err
   107  }
   108  
   109  // Exist check if a bucket or an object exist
   110  // If fileName is empty, then 'Exist' function will only check if the given bucket exist.
   111  func (s *storageWrapper) Exist(ctx context.Context, URI archiver.URI, fileName string) (exists bool, err error) {
   112  	bucket := s.client.Bucket(URI.Hostname())
   113  	if _, err := bucket.Attrs(ctx); err != nil {
   114  		return false, err
   115  	}
   116  
   117  	if fileName == "" {
   118  		return true, nil
   119  	}
   120  
   121  	if _, err = bucket.Object(fileName).Attrs(ctx); err != nil {
   122  		return false, errObjectNotFound
   123  	}
   124  
   125  	return true, nil
   126  }
   127  
   128  // Get retrieve a file
   129  func (s *storageWrapper) Get(ctx context.Context, URI archiver.URI, fileName string) (fileContent []byte, err error) {
   130  	bucket := s.client.Bucket(URI.Hostname())
   131  	reader, err := bucket.Object(formatSinkPath(URI.Path()) + "/" + fileName).NewReader(ctx)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	defer func() {
   136  		err = multierr.Combine(err, reader.Close())
   137  	}()
   138  	return io.ReadAll(reader)
   139  }
   140  
   141  // Query, retieves file names by provided storage query
   142  func (s *storageWrapper) Query(ctx context.Context, URI archiver.URI, fileNamePrefix string) (fileNames []string, err error) {
   143  	fileNames = make([]string, 0)
   144  	bucket := s.client.Bucket(URI.Hostname())
   145  	var attrs = new(storage.ObjectAttrs)
   146  	it := bucket.Objects(ctx, &storage.Query{
   147  		Prefix: formatSinkPath(URI.Path()) + "/" + fileNamePrefix,
   148  	})
   149  
   150  	for {
   151  		attrs, err = it.Next()
   152  		if err == iterator.Done {
   153  			return fileNames, nil
   154  		}
   155  		fileNames = append(fileNames, attrs.Name)
   156  	}
   157  
   158  }
   159  
   160  // QueryWithFilter, retieves filenames that match filter parameters. PageSize is optional, 0 means all records.
   161  func (s *storageWrapper) QueryWithFilters(ctx context.Context, URI archiver.URI, fileNamePrefix string, pageSize, offset int, filters []Precondition) ([]string, bool, int, error) {
   162  	var err error
   163  	currentPos := offset
   164  	resultSet := make([]string, 0)
   165  	bucket := s.client.Bucket(URI.Hostname())
   166  	var attrs = new(storage.ObjectAttrs)
   167  	it := bucket.Objects(ctx, &storage.Query{
   168  		Prefix: formatSinkPath(URI.Path()) + "/" + fileNamePrefix,
   169  	})
   170  
   171  	for {
   172  		attrs, err = it.Next()
   173  		if err == iterator.Done {
   174  			return resultSet, true, currentPos, nil
   175  		}
   176  
   177  		if completed := isPageCompleted(pageSize, len(resultSet)); completed {
   178  			return resultSet, completed, currentPos, err
   179  		}
   180  
   181  		valid := true
   182  		for _, f := range filters {
   183  			if valid = f(attrs.Name); !valid {
   184  				break
   185  			}
   186  		}
   187  
   188  		if valid {
   189  			if offset > 0 {
   190  				offset--
   191  				continue
   192  			}
   193  			// if match parsedQuery criteria and current cursor position is the last known position (offset is zero), append fileName to resultSet
   194  			resultSet = append(resultSet, attrs.Name)
   195  			currentPos++
   196  		}
   197  	}
   198  
   199  }
   200  
   201  func isPageCompleted(pageSize, currentPosition int) bool {
   202  	return pageSize != 0 && currentPosition > 0 && pageSize <= currentPosition
   203  }
   204  
   205  func formatSinkPath(sinkPath string) string {
   206  	return sinkPath[1:]
   207  }