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  }