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 }