github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/storage/cloud/external_storage.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package cloud 12 13 import ( 14 "context" 15 "io" 16 "net/url" 17 "strconv" 18 "strings" 19 "time" 20 21 "github.com/aws/aws-sdk-go/service/s3" 22 "github.com/cockroachdb/cockroach/pkg/base" 23 "github.com/cockroachdb/cockroach/pkg/blobs" 24 "github.com/cockroachdb/cockroach/pkg/roachpb" 25 "github.com/cockroachdb/cockroach/pkg/server/telemetry" 26 "github.com/cockroachdb/cockroach/pkg/settings" 27 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 28 "github.com/cockroachdb/cockroach/pkg/util/retry" 29 "github.com/cockroachdb/cockroach/pkg/util/sysutil" 30 "github.com/cockroachdb/errors" 31 ) 32 33 const ( 34 // S3AccessKeyParam is the query parameter for access_key in an S3 URI. 35 S3AccessKeyParam = "AWS_ACCESS_KEY_ID" 36 // S3SecretParam is the query parameter for the 'secret' in an S3 URI. 37 S3SecretParam = "AWS_SECRET_ACCESS_KEY" 38 // S3TempTokenParam is the query parameter for session_token in an S3 URI. 39 S3TempTokenParam = "AWS_SESSION_TOKEN" 40 // S3EndpointParam is the query parameter for the 'endpoint' in an S3 URI. 41 S3EndpointParam = "AWS_ENDPOINT" 42 // S3RegionParam is the query parameter for the 'endpoint' in an S3 URI. 43 S3RegionParam = "AWS_REGION" 44 45 // AzureAccountNameParam is the query parameter for account_name in an azure URI. 46 AzureAccountNameParam = "AZURE_ACCOUNT_NAME" 47 // AzureAccountKeyParam is the query parameter for account_key in an azure URI. 48 AzureAccountKeyParam = "AZURE_ACCOUNT_KEY" 49 50 // GoogleBillingProjectParam is the query parameter for the billing project 51 // in a gs URI. 52 GoogleBillingProjectParam = "GOOGLE_BILLING_PROJECT" 53 54 // AuthParam is the query parameter for the cluster settings named 55 // key in a URI. 56 AuthParam = "AUTH" 57 authParamImplicit = "implicit" 58 authParamDefault = "default" 59 authParamSpecified = "specified" 60 61 // CredentialsParam is the query parameter for the base64-encoded contents of 62 // the Google Application Credentials JSON file. 63 CredentialsParam = "CREDENTIALS" 64 65 cloudstoragePrefix = "cloudstorage" 66 cloudstorageGS = cloudstoragePrefix + ".gs" 67 cloudstorageHTTP = cloudstoragePrefix + ".http" 68 69 cloudstorageDefault = ".default" 70 cloudstorageKey = ".key" 71 72 cloudstorageGSDefault = cloudstorageGS + cloudstorageDefault 73 cloudstorageGSDefaultKey = cloudstorageGSDefault + cloudstorageKey 74 75 cloudstorageHTTPCASetting = cloudstorageHTTP + ".custom_ca" 76 77 cloudStorageTimeout = cloudstoragePrefix + ".timeout" 78 ) 79 80 // See SanitizeExternalStorageURI. 81 var redactedQueryParams = map[string]struct{}{ 82 S3SecretParam: {}, 83 S3TempTokenParam: {}, 84 AzureAccountKeyParam: {}, 85 CredentialsParam: {}, 86 } 87 88 // ErrListingUnsupported is a marker for indicating listing is unsupported. 89 var ErrListingUnsupported = errors.New("listing is not supported") 90 91 // ExternalStorageFactory describes a factory function for ExternalStorage. 92 type ExternalStorageFactory func(ctx context.Context, dest roachpb.ExternalStorage) (ExternalStorage, error) 93 94 // ExternalStorageFromURIFactory describes a factory function for ExternalStorage given a URI. 95 type ExternalStorageFromURIFactory func(ctx context.Context, uri string) (ExternalStorage, error) 96 97 // ExternalStorage provides functions to read and write files in some storage, 98 // namely various cloud storage providers, for example to store backups. 99 // Generally an implementation is instantiated pointing to some base path or 100 // prefix and then gets and puts files using the various methods to interact 101 // with individual files contained within that path or prefix. 102 // However, implementations must also allow callers to provide the full path to 103 // a given file as the "base" path, and then read or write it with the methods 104 // below by simply passing an empty filename. Implementations that use stdlib's 105 // `filepath.Join` to concatenate their base path with the provided filename will 106 // find its semantics well suited to this -- it elides empty components and does 107 // not append surplus slashes. 108 type ExternalStorage interface { 109 io.Closer 110 111 // Conf should return the serializable configuration required to reconstruct 112 // this ExternalStorage implementation. 113 Conf() roachpb.ExternalStorage 114 115 // ReadFile should return a Reader for requested name. 116 ReadFile(ctx context.Context, basename string) (io.ReadCloser, error) 117 118 // WriteFile should write the content to requested name. 119 WriteFile(ctx context.Context, basename string, content io.ReadSeeker) error 120 121 // ListFiles returns files that match a globs-style pattern. The returned 122 // results are usually relative to the base path, meaning an ExternalStorage 123 // instance can be initialized with some base path, used to query for files, 124 // then pass those results to its other methods. 125 // 126 // As a special-case, if the passed patternSuffix is empty, the base path used 127 // to initialize the storage connection is treated as a pattern. In this case, 128 // as the connection is not really reusable for interacting with other files 129 // and there is no clear definition of what it would mean to be relative to 130 // that, the results are fully-qualified absolute URIs. The base URI is *only* 131 // allowed to contain globs-patterns when the explicit patternSuffix is "". 132 ListFiles(ctx context.Context, patternSuffix string) ([]string, error) 133 134 // Delete removes the named file from the store. 135 Delete(ctx context.Context, basename string) error 136 137 // Size returns the length of the named file in bytes. 138 Size(ctx context.Context, basename string) (int64, error) 139 } 140 141 // ExternalStorageConfFromURI generates an ExternalStorage config from a URI string. 142 func ExternalStorageConfFromURI(path string) (roachpb.ExternalStorage, error) { 143 conf := roachpb.ExternalStorage{} 144 uri, err := url.Parse(path) 145 if err != nil { 146 return conf, err 147 } 148 switch uri.Scheme { 149 case "s3": 150 conf.Provider = roachpb.ExternalStorageProvider_S3 151 conf.S3Config = &roachpb.ExternalStorage_S3{ 152 Bucket: uri.Host, 153 Prefix: uri.Path, 154 AccessKey: uri.Query().Get(S3AccessKeyParam), 155 Secret: uri.Query().Get(S3SecretParam), 156 TempToken: uri.Query().Get(S3TempTokenParam), 157 Endpoint: uri.Query().Get(S3EndpointParam), 158 Region: uri.Query().Get(S3RegionParam), 159 Auth: uri.Query().Get(AuthParam), 160 /* NB: additions here should also update s3QueryParams() serializer */ 161 } 162 conf.S3Config.Prefix = strings.TrimLeft(conf.S3Config.Prefix, "/") 163 // AWS secrets often contain + characters, which must be escaped when 164 // included in a query string; otherwise, they represent a space character. 165 // More than a few users have been bitten by this. 166 // 167 // Luckily, AWS secrets are base64-encoded data and thus will never actually 168 // contain spaces. We can convert any space characters we see to + 169 // characters to recover the original secret. 170 conf.S3Config.Secret = strings.Replace(conf.S3Config.Secret, " ", "+", -1) 171 case "gs": 172 conf.Provider = roachpb.ExternalStorageProvider_GoogleCloud 173 conf.GoogleCloudConfig = &roachpb.ExternalStorage_GCS{ 174 Bucket: uri.Host, 175 Prefix: uri.Path, 176 Auth: uri.Query().Get(AuthParam), 177 BillingProject: uri.Query().Get(GoogleBillingProjectParam), 178 Credentials: uri.Query().Get(CredentialsParam), 179 /* NB: additions here should also update gcsQueryParams() serializer */ 180 } 181 conf.GoogleCloudConfig.Prefix = strings.TrimLeft(conf.GoogleCloudConfig.Prefix, "/") 182 case "azure": 183 conf.Provider = roachpb.ExternalStorageProvider_Azure 184 conf.AzureConfig = &roachpb.ExternalStorage_Azure{ 185 Container: uri.Host, 186 Prefix: uri.Path, 187 AccountName: uri.Query().Get(AzureAccountNameParam), 188 AccountKey: uri.Query().Get(AzureAccountKeyParam), 189 /* NB: additions here should also update azureQueryParams() serializer */ 190 } 191 if conf.AzureConfig.AccountName == "" { 192 return conf, errors.Errorf("azure uri missing %q parameter", AzureAccountNameParam) 193 } 194 if conf.AzureConfig.AccountKey == "" { 195 return conf, errors.Errorf("azure uri missing %q parameter", AzureAccountKeyParam) 196 } 197 conf.AzureConfig.Prefix = strings.TrimLeft(conf.AzureConfig.Prefix, "/") 198 case "http", "https": 199 conf.Provider = roachpb.ExternalStorageProvider_Http 200 conf.HttpPath.BaseUri = path 201 case "nodelocal": 202 if uri.Host == "" { 203 return conf, errors.Errorf( 204 "host component of nodelocal URI must be a node ID (" + 205 "use 'self' to specify each node should access its own local filesystem)", 206 ) 207 } else if uri.Host == "self" { 208 uri.Host = "0" 209 } 210 211 nodeID, err := strconv.Atoi(uri.Host) 212 if err != nil { 213 return conf, errors.Errorf("host component of nodelocal URI must be a node ID: %s", path) 214 } 215 conf.Provider = roachpb.ExternalStorageProvider_LocalFile 216 conf.LocalFile.Path = uri.Path 217 conf.LocalFile.NodeID = roachpb.NodeID(nodeID) 218 case "experimental-workload", "workload": 219 conf.Provider = roachpb.ExternalStorageProvider_Workload 220 if conf.WorkloadConfig, err = ParseWorkloadConfig(uri); err != nil { 221 return conf, err 222 } 223 default: 224 return conf, errors.Errorf("unsupported storage scheme: %q", uri.Scheme) 225 } 226 return conf, nil 227 } 228 229 // ExternalStorageFromURI returns an ExternalStorage for the given URI. 230 func ExternalStorageFromURI( 231 ctx context.Context, 232 uri string, 233 externalConfig base.ExternalIODirConfig, 234 settings *cluster.Settings, 235 blobClientFactory blobs.BlobClientFactory, 236 ) (ExternalStorage, error) { 237 conf, err := ExternalStorageConfFromURI(uri) 238 if err != nil { 239 return nil, err 240 } 241 return MakeExternalStorage(ctx, conf, externalConfig, settings, blobClientFactory) 242 } 243 244 // SanitizeExternalStorageURI returns the external storage URI with with some 245 // secrets redacted, for use when showing these URIs in the UI, to provide some 246 // protection from shoulder-surfing. The param is still present -- just 247 // redacted -- to make it clearer that that value is indeed persisted interally. 248 // extraParams which should be scrubbed -- for params beyond those that the 249 // various clound-storage URIs supported by this package know about -- can be 250 // passed allowing this function to be used to scrub other URIs too (such as 251 // non-cloudstorage changefeed sinks). 252 func SanitizeExternalStorageURI(path string, extraParams []string) (string, error) { 253 uri, err := url.Parse(path) 254 if err != nil { 255 return "", err 256 } 257 if uri.Scheme == "experimental-workload" || uri.Scheme == "workload" { 258 return path, nil 259 } 260 261 params := uri.Query() 262 for param := range params { 263 if _, ok := redactedQueryParams[param]; ok { 264 params.Set(param, "redacted") 265 } else { 266 for _, p := range extraParams { 267 if param == p { 268 params.Set(param, "redacted") 269 } 270 } 271 } 272 } 273 274 uri.RawQuery = params.Encode() 275 return uri.String(), nil 276 } 277 278 // MakeExternalStorage creates an ExternalStorage from the given config. 279 func MakeExternalStorage( 280 ctx context.Context, 281 dest roachpb.ExternalStorage, 282 conf base.ExternalIODirConfig, 283 settings *cluster.Settings, 284 blobClientFactory blobs.BlobClientFactory, 285 ) (ExternalStorage, error) { 286 switch dest.Provider { 287 case roachpb.ExternalStorageProvider_LocalFile: 288 telemetry.Count("external-io.nodelocal") 289 return makeLocalStorage(ctx, dest.LocalFile, settings, blobClientFactory) 290 case roachpb.ExternalStorageProvider_Http: 291 if conf.DisableHTTP { 292 return nil, errors.New("external http access disabled") 293 } 294 telemetry.Count("external-io.http") 295 return makeHTTPStorage(dest.HttpPath.BaseUri, settings) 296 case roachpb.ExternalStorageProvider_S3: 297 telemetry.Count("external-io.s3") 298 return makeS3Storage(ctx, conf, dest.S3Config, settings) 299 case roachpb.ExternalStorageProvider_GoogleCloud: 300 telemetry.Count("external-io.google_cloud") 301 return makeGCSStorage(ctx, conf, dest.GoogleCloudConfig, settings) 302 case roachpb.ExternalStorageProvider_Azure: 303 telemetry.Count("external-io.azure") 304 return makeAzureStorage(dest.AzureConfig, settings) 305 case roachpb.ExternalStorageProvider_Workload: 306 telemetry.Count("external-io.workload") 307 return makeWorkloadStorage(dest.WorkloadConfig) 308 } 309 return nil, errors.Errorf("unsupported external destination type: %s", dest.Provider.String()) 310 } 311 312 // URINeedsGlobExpansion checks if URI can be expanded by checking if it contains wildcard characters. 313 // This should be used before passing a URI into ListFiles(). 314 func URINeedsGlobExpansion(uri string) bool { 315 parsedURI, err := url.Parse(uri) 316 if err != nil { 317 return false 318 } 319 // We don't support listing files for workload and http. 320 unsupported := []string{"workload", "http", "https", "experimental-workload"} 321 for _, str := range unsupported { 322 if parsedURI.Scheme == str { 323 return false 324 } 325 } 326 327 return containsGlob(parsedURI.Path) 328 } 329 330 func containsGlob(str string) bool { 331 return strings.ContainsAny(str, "*?[") 332 } 333 334 var ( 335 gcsDefault = settings.RegisterPublicStringSetting( 336 cloudstorageGSDefaultKey, 337 "if set, JSON key to use during Google Cloud Storage operations", 338 "", 339 ) 340 httpCustomCA = settings.RegisterPublicStringSetting( 341 cloudstorageHTTPCASetting, 342 "custom root CA (appended to system's default CAs) for verifying certificates when interacting with HTTPS storage", 343 "", 344 ) 345 timeoutSetting = settings.RegisterPublicDurationSetting( 346 cloudStorageTimeout, 347 "the timeout for import/export storage operations", 348 10*time.Minute) 349 ) 350 351 // delayedRetry runs fn and re-runs it a limited number of times if it 352 // fails. It knows about specific kinds of errors that need longer retry 353 // delays than normal. 354 func delayedRetry(ctx context.Context, fn func() error) error { 355 const maxAttempts = 3 356 return retry.WithMaxAttempts(ctx, base.DefaultRetryOptions(), maxAttempts, func() error { 357 err := fn() 358 if err == nil { 359 return nil 360 } 361 var s3err s3.RequestFailure 362 if errors.As(err, &s3err) { 363 // A 503 error could mean we need to reduce our request rate. Impose an 364 // arbitrary slowdown in that case. 365 // See http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html 366 if s3err.StatusCode() == 503 { 367 select { 368 case <-time.After(time.Second * 5): 369 case <-ctx.Done(): 370 } 371 } 372 } 373 // See https:github.com/GoogleCloudPlatform/google-cloud-go/issues/1012#issuecomment-393606797 374 // which suggests this GCE error message could be due to auth quota limits 375 // being reached. 376 if strings.Contains(err.Error(), "net/http: timeout awaiting response headers") { 377 select { 378 case <-time.After(time.Second * 5): 379 case <-ctx.Done(): 380 } 381 } 382 return err 383 }) 384 } 385 386 // isResumableHTTPError returns true if we can 387 // resume download after receiving an error 'err'. 388 // We can attempt to resume download if the error is ErrUnexpectedEOF. 389 // In particular, we should not worry about a case when error is io.EOF. 390 // The reason for this is two-fold: 391 // 1. The underlying http library converts io.EOF to io.ErrUnexpectedEOF 392 // if the number of bytes transferred is less than the number of 393 // bytes advertised in the Content-Length header. So if we see 394 // io.ErrUnexpectedEOF we can simply request the next range. 395 // 2. If the server did *not* advertise Content-Length, then 396 // there is really nothing we can do: http standard says that 397 // the stream ends when the server terminates connection. 398 // In addition, we treat connection reset by peer errors (which can 399 // happen if we didn't read from the connection too long due to e.g. load), 400 // the same as unexpected eof errors. 401 func isResumableHTTPError(err error) bool { 402 return errors.Is(err, io.ErrUnexpectedEOF) || 403 sysutil.IsErrConnectionReset(err) || 404 sysutil.IsErrConnectionRefused(err) 405 } 406 407 // Maximum number of times we can attempt to retry reading from external storage, 408 // without making any progress. 409 const maxNoProgressReads = 3