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