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 }