github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/internal/s3client/client.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package s3client 5 6 import ( 7 "context" 8 "io" 9 "net/http" 10 11 "github.com/aws/aws-sdk-go-v2/aws" 12 "github.com/aws/aws-sdk-go-v2/aws/retry" 13 "github.com/aws/aws-sdk-go-v2/config" 14 "github.com/aws/aws-sdk-go-v2/service/s3" 15 "github.com/aws/smithy-go/logging" 16 "github.com/juju/errors" 17 "gopkg.in/httprequest.v1" 18 19 "github.com/juju/juju/api" 20 ) 21 22 // Logger represents the logging methods called. 23 type Logger interface { 24 Errorf(message string, args ...interface{}) 25 Warningf(message string, args ...interface{}) 26 Infof(message string, args ...interface{}) 27 Debugf(message string, args ...interface{}) 28 Tracef(message string, args ...interface{}) 29 } 30 31 // S3Client represents the S3 client methods required by objectClient 32 type S3Client interface { 33 GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) 34 } 35 36 // Session represents the interface objectClient exports to interact with S3 37 type Session interface { 38 GetObject(ctx context.Context, bucketName, objectName string) (io.ReadCloser, error) 39 } 40 41 // objectsClient is a Juju shim around the AWS S3 client, 42 // which Juju uses to drive it's object store requirents 43 type objectsClient struct { 44 logger Logger 45 client S3Client 46 } 47 48 // GetObject retrieves an object from an S3 object store. Returns a 49 // stream containing the object's content 50 func (c *objectsClient) GetObject(ctx context.Context, bucketName, objectName string) (io.ReadCloser, error) { 51 c.logger.Tracef("retrieving bucket %s object %s from s3 storage", bucketName, objectName) 52 53 obj, err := c.client.GetObject(ctx, 54 &s3.GetObjectInput{ 55 Bucket: aws.String(bucketName), 56 Key: aws.String(objectName), 57 }) 58 if err != nil { 59 return nil, errors.Annotatef(err, "unable to get object %s on bucket %s using S3 client", objectName, bucketName) 60 } 61 return obj.Body, nil 62 } 63 64 type awsEndpointResolver struct { 65 endpoint string 66 } 67 68 func (a *awsEndpointResolver) ResolveEndpoint(_, _ string) (aws.Endpoint, error) { 69 return aws.Endpoint{ 70 URL: a.endpoint, 71 }, nil 72 } 73 74 type awsHTTPDoer struct { 75 client *httprequest.Client 76 } 77 78 func (c *awsHTTPDoer) Do(req *http.Request) (*http.Response, error) { 79 var res *http.Response 80 err := c.client.Do(context.Background(), req, &res) 81 82 return res, err 83 } 84 85 type awsLogger struct { 86 logger Logger 87 } 88 89 func (l *awsLogger) Logf(classification logging.Classification, format string, v ...interface{}) { 90 switch classification { 91 case logging.Warn: 92 l.logger.Warningf(format, v) 93 case logging.Debug: 94 l.logger.Debugf(format, v) 95 default: 96 l.logger.Tracef(format, v) 97 } 98 } 99 100 type unlimitedRateLimiter struct{} 101 102 func (unlimitedRateLimiter) AddTokens(uint) error { return nil } 103 func (unlimitedRateLimiter) GetToken(context.Context, uint) (func() error, error) { 104 return noOpToken, nil 105 } 106 func noOpToken() error { return nil } 107 108 // NewS3Client creates a generic S3 client which Juju should use to 109 // drive it's object store requirements 110 func NewS3Client(apiConn api.Connection, logger Logger) (Session, error) { 111 apiHTTPClient, err := apiConn.RootHTTPClient() 112 if err != nil { 113 return nil, errors.Annotate(err, "cannot retrieve http client from the api connection") 114 } 115 awsHTTPDoer := &awsHTTPDoer{ 116 client: apiHTTPClient, 117 } 118 awsLogger := &awsLogger{ 119 logger: logger, 120 } 121 122 cfg, err := config.LoadDefaultConfig( 123 context.Background(), 124 config.WithLogger(awsLogger), 125 config.WithHTTPClient(awsHTTPDoer), 126 config.WithEndpointResolver(&awsEndpointResolver{endpoint: apiHTTPClient.BaseURL}), 127 // Standard retryer with custom max attempts. Will retry at most 128 // 10 times with 20s backoff time. 129 config.WithRetryer(func() aws.Retryer { 130 return retry.NewStandard( 131 func(o *retry.StandardOptions) { 132 o.MaxAttempts = 10 133 o.RateLimiter = unlimitedRateLimiter{} 134 }, 135 ) 136 }), 137 // The anonymous credentials are needed by the aws sdk to 138 // perform anonymous s3 access. 139 config.WithCredentialsProvider(aws.AnonymousCredentials{}), 140 ) 141 if err != nil { 142 return nil, errors.Annotate(err, "cannot load default config for s3 client") 143 } 144 145 return &objectsClient{ 146 client: s3.NewFromConfig(cfg, func(o *s3.Options) { 147 o.UsePathStyle = true 148 }), 149 logger: logger, 150 }, nil 151 }