github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/chunk/client/aws/s3_storage_client.go (about)

     1  package aws
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"flag"
     8  	"fmt"
     9  	"hash/fnv"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/aws/aws-sdk-go/aws"
    18  	"github.com/aws/aws-sdk-go/aws/awserr"
    19  	"github.com/aws/aws-sdk-go/aws/credentials"
    20  	"github.com/aws/aws-sdk-go/aws/request"
    21  	"github.com/aws/aws-sdk-go/aws/session"
    22  	v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
    23  	"github.com/aws/aws-sdk-go/service/s3"
    24  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    25  	"github.com/grafana/dskit/backoff"
    26  	"github.com/grafana/dskit/flagext"
    27  	"github.com/minio/minio-go/v7/pkg/signer"
    28  	"github.com/pkg/errors"
    29  	"github.com/prometheus/client_golang/prometheus"
    30  	awscommon "github.com/weaveworks/common/aws"
    31  	"github.com/weaveworks/common/instrument"
    32  
    33  	bucket_s3 "github.com/grafana/loki/pkg/storage/bucket/s3"
    34  	"github.com/grafana/loki/pkg/storage/chunk/client"
    35  	"github.com/grafana/loki/pkg/storage/chunk/client/hedging"
    36  	"github.com/grafana/loki/pkg/util"
    37  )
    38  
    39  const (
    40  	SignatureVersionV4 = "v4"
    41  	SignatureVersionV2 = "v2"
    42  )
    43  
    44  var (
    45  	supportedSignatureVersions     = []string{SignatureVersionV4, SignatureVersionV2}
    46  	errUnsupportedSignatureVersion = errors.New("unsupported signature version")
    47  )
    48  
    49  var s3RequestDuration = instrument.NewHistogramCollector(prometheus.NewHistogramVec(prometheus.HistogramOpts{
    50  	Namespace: "loki",
    51  	Name:      "s3_request_duration_seconds",
    52  	Help:      "Time spent doing S3 requests.",
    53  	Buckets:   []float64{.025, .05, .1, .25, .5, 1, 2},
    54  }, []string{"operation", "status_code"}))
    55  
    56  // InjectRequestMiddleware gives users of this client the ability to make arbitrary
    57  // changes to outgoing requests.
    58  type InjectRequestMiddleware func(next http.RoundTripper) http.RoundTripper
    59  
    60  func init() {
    61  	s3RequestDuration.Register()
    62  }
    63  
    64  // S3Config specifies config for storing chunks on AWS S3.
    65  type S3Config struct {
    66  	S3               flagext.URLValue
    67  	S3ForcePathStyle bool
    68  
    69  	BucketNames      string
    70  	Endpoint         string              `yaml:"endpoint"`
    71  	Region           string              `yaml:"region"`
    72  	AccessKeyID      string              `yaml:"access_key_id"`
    73  	SecretAccessKey  flagext.Secret      `yaml:"secret_access_key"`
    74  	Insecure         bool                `yaml:"insecure"`
    75  	SSEEncryption    bool                `yaml:"sse_encryption"`
    76  	HTTPConfig       HTTPConfig          `yaml:"http_config"`
    77  	SignatureVersion string              `yaml:"signature_version"`
    78  	SSEConfig        bucket_s3.SSEConfig `yaml:"sse"`
    79  	BackoffConfig    backoff.Config      `yaml:"backoff_config"`
    80  
    81  	Inject InjectRequestMiddleware `yaml:"-"`
    82  }
    83  
    84  // HTTPConfig stores the http.Transport configuration
    85  type HTTPConfig struct {
    86  	IdleConnTimeout       time.Duration `yaml:"idle_conn_timeout"`
    87  	ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout"`
    88  	InsecureSkipVerify    bool          `yaml:"insecure_skip_verify"`
    89  	CAFile                string        `yaml:"ca_file"`
    90  }
    91  
    92  // RegisterFlags adds the flags required to config this to the given FlagSet
    93  func (cfg *S3Config) RegisterFlags(f *flag.FlagSet) {
    94  	cfg.RegisterFlagsWithPrefix("", f)
    95  }
    96  
    97  // RegisterFlagsWithPrefix adds the flags required to config this to the given FlagSet with a specified prefix
    98  func (cfg *S3Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
    99  	f.Var(&cfg.S3, prefix+"s3.url", "S3 endpoint URL with escaped Key and Secret encoded. "+
   100  		"If only region is specified as a host, proper endpoint will be deduced. Use inmemory:///<bucket-name> to use a mock in-memory implementation.")
   101  	f.BoolVar(&cfg.S3ForcePathStyle, prefix+"s3.force-path-style", false, "Set this to `true` to force the request to use path-style addressing.")
   102  	f.StringVar(&cfg.BucketNames, prefix+"s3.buckets", "", "Comma separated list of bucket names to evenly distribute chunks over. Overrides any buckets specified in s3.url flag")
   103  
   104  	f.StringVar(&cfg.Endpoint, prefix+"s3.endpoint", "", "S3 Endpoint to connect to.")
   105  	f.StringVar(&cfg.Region, prefix+"s3.region", "", "AWS region to use.")
   106  	f.StringVar(&cfg.AccessKeyID, prefix+"s3.access-key-id", "", "AWS Access Key ID")
   107  	f.Var(&cfg.SecretAccessKey, prefix+"s3.secret-access-key", "AWS Secret Access Key")
   108  	f.BoolVar(&cfg.Insecure, prefix+"s3.insecure", false, "Disable https on s3 connection.")
   109  
   110  	// TODO Remove in Cortex 1.10.0
   111  	f.BoolVar(&cfg.SSEEncryption, prefix+"s3.sse-encryption", false, "Enable AWS Server Side Encryption [Deprecated: Use .sse instead. if s3.sse-encryption is enabled, it assumes .sse.type SSE-S3]")
   112  
   113  	cfg.SSEConfig.RegisterFlagsWithPrefix(prefix+"s3.sse.", f)
   114  
   115  	f.DurationVar(&cfg.HTTPConfig.IdleConnTimeout, prefix+"s3.http.idle-conn-timeout", 90*time.Second, "The maximum amount of time an idle connection will be held open.")
   116  	f.DurationVar(&cfg.HTTPConfig.ResponseHeaderTimeout, prefix+"s3.http.response-header-timeout", 0, "If non-zero, specifies the amount of time to wait for a server's response headers after fully writing the request.")
   117  	f.BoolVar(&cfg.HTTPConfig.InsecureSkipVerify, prefix+"s3.http.insecure-skip-verify", false, "Set to true to skip verifying the certificate chain and hostname.")
   118  	f.StringVar(&cfg.HTTPConfig.CAFile, prefix+"s3.http.ca-file", "", "Path to the trusted CA file that signed the SSL certificate of the S3 endpoint.")
   119  	f.StringVar(&cfg.SignatureVersion, prefix+"s3.signature-version", SignatureVersionV4, fmt.Sprintf("The signature version to use for authenticating against S3. Supported values are: %s.", strings.Join(supportedSignatureVersions, ", ")))
   120  
   121  	f.DurationVar(&cfg.BackoffConfig.MinBackoff, prefix+"s3.min-backoff", 100*time.Millisecond, "Minimum backoff time when s3 get Object")
   122  	f.DurationVar(&cfg.BackoffConfig.MaxBackoff, prefix+"s3.max-backoff", 3*time.Second, "Maximum backoff time when s3 get Object")
   123  	f.IntVar(&cfg.BackoffConfig.MaxRetries, prefix+"s3.max-retries", 5, "Maximum number of times to retry when s3 get Object")
   124  }
   125  
   126  // Validate config and returns error on failure
   127  func (cfg *S3Config) Validate() error {
   128  	if !util.StringsContain(supportedSignatureVersions, cfg.SignatureVersion) {
   129  		return errUnsupportedSignatureVersion
   130  	}
   131  	return nil
   132  }
   133  
   134  type S3ObjectClient struct {
   135  	cfg S3Config
   136  
   137  	bucketNames []string
   138  	S3          s3iface.S3API
   139  	hedgedS3    s3iface.S3API
   140  	sseConfig   *SSEParsedConfig
   141  }
   142  
   143  // NewS3ObjectClient makes a new S3-backed ObjectClient.
   144  func NewS3ObjectClient(cfg S3Config, hedgingCfg hedging.Config) (*S3ObjectClient, error) {
   145  	bucketNames, err := buckets(cfg)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	s3Client, err := buildS3Client(cfg, hedgingCfg, false)
   150  	if err != nil {
   151  		return nil, errors.Wrap(err, "failed to build s3 config")
   152  	}
   153  	s3ClientHedging, err := buildS3Client(cfg, hedgingCfg, true)
   154  	if err != nil {
   155  		return nil, errors.Wrap(err, "failed to build s3 config")
   156  	}
   157  
   158  	sseCfg, err := buildSSEParsedConfig(cfg)
   159  	if err != nil {
   160  		return nil, errors.Wrap(err, "failed to build SSE config")
   161  	}
   162  
   163  	client := S3ObjectClient{
   164  		cfg:         cfg,
   165  		S3:          s3Client,
   166  		hedgedS3:    s3ClientHedging,
   167  		bucketNames: bucketNames,
   168  		sseConfig:   sseCfg,
   169  	}
   170  	return &client, nil
   171  }
   172  
   173  func buildSSEParsedConfig(cfg S3Config) (*SSEParsedConfig, error) {
   174  	if cfg.SSEConfig.Type != "" {
   175  		return NewSSEParsedConfig(cfg.SSEConfig)
   176  	}
   177  
   178  	// deprecated, but if used it assumes SSE-S3 type
   179  	if cfg.SSEEncryption {
   180  		return NewSSEParsedConfig(bucket_s3.SSEConfig{
   181  			Type: bucket_s3.SSES3,
   182  		})
   183  	}
   184  
   185  	return nil, nil
   186  }
   187  
   188  func v2SignRequestHandler(cfg S3Config) request.NamedHandler {
   189  	return request.NamedHandler{
   190  		Name: "v2.SignRequestHandler",
   191  		Fn: func(req *request.Request) {
   192  			credentials, err := req.Config.Credentials.GetWithContext(req.Context())
   193  			if err != nil {
   194  				if err != nil {
   195  					req.Error = err
   196  					return
   197  				}
   198  			}
   199  
   200  			req.HTTPRequest = signer.SignV2(
   201  				*req.HTTPRequest,
   202  				credentials.AccessKeyID,
   203  				credentials.SecretAccessKey,
   204  				!cfg.S3ForcePathStyle,
   205  			)
   206  		},
   207  	}
   208  }
   209  
   210  func buildS3Client(cfg S3Config, hedgingCfg hedging.Config, hedging bool) (*s3.S3, error) {
   211  	var s3Config *aws.Config
   212  	var err error
   213  
   214  	// if an s3 url is passed use it to initialize the s3Config and then override with any additional params
   215  	if cfg.S3.URL != nil {
   216  		s3Config, err = awscommon.ConfigFromURL(cfg.S3.URL)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  	} else {
   221  		s3Config = &aws.Config{}
   222  		s3Config = s3Config.WithRegion("dummy")
   223  	}
   224  
   225  	s3Config = s3Config.WithMaxRetries(0)                          // We do our own retries, so we can monitor them
   226  	s3Config = s3Config.WithS3ForcePathStyle(cfg.S3ForcePathStyle) // support for Path Style S3 url if has the flag
   227  
   228  	if cfg.Endpoint != "" {
   229  		s3Config = s3Config.WithEndpoint(cfg.Endpoint)
   230  	}
   231  
   232  	if cfg.Insecure {
   233  		s3Config = s3Config.WithDisableSSL(true)
   234  	}
   235  
   236  	if cfg.Region != "" {
   237  		s3Config = s3Config.WithRegion(cfg.Region)
   238  	}
   239  
   240  	if cfg.AccessKeyID != "" && cfg.SecretAccessKey.String() == "" ||
   241  		cfg.AccessKeyID == "" && cfg.SecretAccessKey.String() != "" {
   242  		return nil, errors.New("must supply both an Access Key ID and Secret Access Key or neither")
   243  	}
   244  
   245  	if cfg.AccessKeyID != "" && cfg.SecretAccessKey.String() != "" {
   246  		creds := credentials.NewStaticCredentials(cfg.AccessKeyID, cfg.SecretAccessKey.String(), "")
   247  		s3Config = s3Config.WithCredentials(creds)
   248  	}
   249  
   250  	tlsConfig := &tls.Config{
   251  		InsecureSkipVerify: cfg.HTTPConfig.InsecureSkipVerify,
   252  	}
   253  
   254  	if cfg.HTTPConfig.CAFile != "" {
   255  		tlsConfig.RootCAs = x509.NewCertPool()
   256  		data, err := os.ReadFile(cfg.HTTPConfig.CAFile)
   257  		if err != nil {
   258  			return nil, err
   259  		}
   260  		tlsConfig.RootCAs.AppendCertsFromPEM(data)
   261  	}
   262  
   263  	// While extending S3 configuration this http config was copied in order to
   264  	// to maintain backwards compatibility with previous versions of Cortex while providing
   265  	// more flexible configuration of the http client
   266  	// https://github.com/weaveworks/common/blob/4b1847531bc94f54ce5cf210a771b2a86cd34118/aws/config.go#L23
   267  	transport := http.RoundTripper(&http.Transport{
   268  		Proxy: http.ProxyFromEnvironment,
   269  		DialContext: (&net.Dialer{
   270  			Timeout:   30 * time.Second,
   271  			KeepAlive: 30 * time.Second,
   272  			DualStack: true,
   273  		}).DialContext,
   274  		MaxIdleConns:          200,
   275  		IdleConnTimeout:       cfg.HTTPConfig.IdleConnTimeout,
   276  		MaxIdleConnsPerHost:   200,
   277  		TLSHandshakeTimeout:   3 * time.Second,
   278  		ExpectContinueTimeout: 1 * time.Second,
   279  		ResponseHeaderTimeout: cfg.HTTPConfig.ResponseHeaderTimeout,
   280  		TLSClientConfig:       tlsConfig,
   281  	})
   282  
   283  	if cfg.Inject != nil {
   284  		transport = cfg.Inject(transport)
   285  	}
   286  	httpClient := &http.Client{
   287  		Transport: transport,
   288  	}
   289  
   290  	if hedging {
   291  		httpClient, err = hedgingCfg.ClientWithRegisterer(httpClient, prometheus.WrapRegistererWithPrefix("loki_", prometheus.DefaultRegisterer))
   292  		if err != nil {
   293  			return nil, err
   294  		}
   295  	}
   296  
   297  	s3Config = s3Config.WithHTTPClient(httpClient)
   298  
   299  	sess, err := session.NewSession(s3Config)
   300  	if err != nil {
   301  		return nil, errors.Wrap(err, "failed to create new s3 session")
   302  	}
   303  
   304  	s3Client := s3.New(sess)
   305  
   306  	if cfg.SignatureVersion == SignatureVersionV2 {
   307  		s3Client.Handlers.Sign.Swap(v4.SignRequestHandler.Name, v2SignRequestHandler(cfg))
   308  	}
   309  
   310  	return s3Client, nil
   311  }
   312  
   313  func buckets(cfg S3Config) ([]string, error) {
   314  	// bucketnames
   315  	var bucketNames []string
   316  	if cfg.S3.URL != nil {
   317  		bucketNames = []string{strings.TrimPrefix(cfg.S3.URL.Path, "/")}
   318  	}
   319  
   320  	if cfg.BucketNames != "" {
   321  		bucketNames = strings.Split(cfg.BucketNames, ",") // comma separated list of bucket names
   322  	}
   323  
   324  	if len(bucketNames) == 0 {
   325  		return nil, errors.New("at least one bucket name must be specified")
   326  	}
   327  	return bucketNames, nil
   328  }
   329  
   330  // Stop fulfills the chunk.ObjectClient interface
   331  func (a *S3ObjectClient) Stop() {}
   332  
   333  // DeleteObject deletes the specified objectKey from the appropriate S3 bucket
   334  func (a *S3ObjectClient) DeleteObject(ctx context.Context, objectKey string) error {
   335  	return instrument.CollectedRequest(ctx, "S3.DeleteObject", s3RequestDuration, instrument.ErrorCode, func(ctx context.Context) error {
   336  		deleteObjectInput := &s3.DeleteObjectInput{
   337  			Bucket: aws.String(a.bucketFromKey(objectKey)),
   338  			Key:    aws.String(objectKey),
   339  		}
   340  
   341  		_, err := a.S3.DeleteObjectWithContext(ctx, deleteObjectInput)
   342  		return err
   343  	})
   344  }
   345  
   346  // bucketFromKey maps a key to a bucket name
   347  func (a *S3ObjectClient) bucketFromKey(key string) string {
   348  	if len(a.bucketNames) == 0 {
   349  		return ""
   350  	}
   351  
   352  	hasher := fnv.New32a()
   353  	hasher.Write([]byte(key)) //nolint: errcheck
   354  	hash := hasher.Sum32()
   355  
   356  	return a.bucketNames[hash%uint32(len(a.bucketNames))]
   357  }
   358  
   359  // GetObject returns a reader and the size for the specified object key from the configured S3 bucket.
   360  func (a *S3ObjectClient) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, int64, error) {
   361  	var resp *s3.GetObjectOutput
   362  
   363  	// Map the key into a bucket
   364  	bucket := a.bucketFromKey(objectKey)
   365  
   366  	retries := backoff.New(ctx, a.cfg.BackoffConfig)
   367  	err := ctx.Err()
   368  	for retries.Ongoing() {
   369  		if ctx.Err() != nil {
   370  			return nil, 0, errors.Wrap(ctx.Err(), "ctx related error during s3 getObject")
   371  		}
   372  		err = instrument.CollectedRequest(ctx, "S3.GetObject", s3RequestDuration, instrument.ErrorCode, func(ctx context.Context) error {
   373  			var requestErr error
   374  			resp, requestErr = a.hedgedS3.GetObjectWithContext(ctx, &s3.GetObjectInput{
   375  				Bucket: aws.String(bucket),
   376  				Key:    aws.String(objectKey),
   377  			})
   378  			return requestErr
   379  		})
   380  		var size int64
   381  		if resp.ContentLength != nil {
   382  			size = *resp.ContentLength
   383  		}
   384  		if err == nil {
   385  			return resp.Body, size, nil
   386  		}
   387  		retries.Wait()
   388  	}
   389  	return nil, 0, errors.Wrap(err, "failed to get s3 object")
   390  }
   391  
   392  // PutObject into the store
   393  func (a *S3ObjectClient) PutObject(ctx context.Context, objectKey string, object io.ReadSeeker) error {
   394  	return instrument.CollectedRequest(ctx, "S3.PutObject", s3RequestDuration, instrument.ErrorCode, func(ctx context.Context) error {
   395  		putObjectInput := &s3.PutObjectInput{
   396  			Body:   object,
   397  			Bucket: aws.String(a.bucketFromKey(objectKey)),
   398  			Key:    aws.String(objectKey),
   399  		}
   400  
   401  		if a.sseConfig != nil {
   402  			putObjectInput.ServerSideEncryption = aws.String(a.sseConfig.ServerSideEncryption)
   403  			putObjectInput.SSEKMSKeyId = a.sseConfig.KMSKeyID
   404  			putObjectInput.SSEKMSEncryptionContext = a.sseConfig.KMSEncryptionContext
   405  		}
   406  
   407  		_, err := a.S3.PutObjectWithContext(ctx, putObjectInput)
   408  		return err
   409  	})
   410  }
   411  
   412  // List implements chunk.ObjectClient.
   413  func (a *S3ObjectClient) List(ctx context.Context, prefix, delimiter string) ([]client.StorageObject, []client.StorageCommonPrefix, error) {
   414  	var storageObjects []client.StorageObject
   415  	var commonPrefixes []client.StorageCommonPrefix
   416  
   417  	for i := range a.bucketNames {
   418  		err := instrument.CollectedRequest(ctx, "S3.List", s3RequestDuration, instrument.ErrorCode, func(ctx context.Context) error {
   419  			input := s3.ListObjectsV2Input{
   420  				Bucket:    aws.String(a.bucketNames[i]),
   421  				Prefix:    aws.String(prefix),
   422  				Delimiter: aws.String(delimiter),
   423  			}
   424  
   425  			for {
   426  				output, err := a.S3.ListObjectsV2WithContext(ctx, &input)
   427  				if err != nil {
   428  					return err
   429  				}
   430  
   431  				for _, content := range output.Contents {
   432  					storageObjects = append(storageObjects, client.StorageObject{
   433  						Key:        *content.Key,
   434  						ModifiedAt: *content.LastModified,
   435  					})
   436  				}
   437  
   438  				for _, commonPrefix := range output.CommonPrefixes {
   439  					commonPrefixes = append(commonPrefixes, client.StorageCommonPrefix(aws.StringValue(commonPrefix.Prefix)))
   440  				}
   441  
   442  				if output.IsTruncated == nil || !*output.IsTruncated {
   443  					// No more results to fetch
   444  					break
   445  				}
   446  				if output.NextContinuationToken == nil {
   447  					// No way to continue
   448  					break
   449  				}
   450  				input.SetContinuationToken(*output.NextContinuationToken)
   451  			}
   452  
   453  			return nil
   454  		})
   455  		if err != nil {
   456  			return nil, nil, err
   457  		}
   458  	}
   459  
   460  	return storageObjects, commonPrefixes, nil
   461  }
   462  
   463  // IsObjectNotFoundErr returns true if error means that object is not found. Relevant to GetObject and DeleteObject operations.
   464  func (a *S3ObjectClient) IsObjectNotFoundErr(err error) bool {
   465  	if aerr, ok := errors.Cause(err).(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey {
   466  		return true
   467  	}
   468  
   469  	return false
   470  }