github.com/jenkins-x/jx/v2@v2.1.155/pkg/cloud/amazon/storage/bucket_provider.go (about)

     1  package storage
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/url"
     8  	"os"
     9  	"strings"
    10  
    11  	session2 "github.com/jenkins-x/jx/v2/pkg/cloud/amazon/session"
    12  
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/aws/session"
    15  	"github.com/aws/aws-sdk-go/service/s3"
    16  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    17  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    18  	"github.com/aws/aws-sdk-go/service/s3/s3manager/s3manageriface"
    19  	"github.com/google/uuid"
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/jenkins-x/jx/v2/pkg/cloud/buckets"
    22  	"github.com/jenkins-x/jx/v2/pkg/config"
    23  	"github.com/jenkins-x/jx/v2/pkg/util"
    24  	"github.com/pkg/errors"
    25  )
    26  
    27  // AmazonBucketProvider the bucket provider for AWS
    28  type AmazonBucketProvider struct {
    29  	Requirements *config.RequirementsConfig
    30  	api          s3iface.S3API
    31  	uploader     s3manageriface.UploaderAPI
    32  	downloader   s3manageriface.DownloaderAPI
    33  }
    34  
    35  func (b AmazonBucketProvider) createAWSSession() (*session.Session, error) {
    36  	region := b.Requirements.Cluster.Region
    37  	if region == "" {
    38  		return nil, errors.New("requirements do not specify a cluster region")
    39  	}
    40  	sess, err := session2.NewAwsSession("", region)
    41  	if err != nil {
    42  		return nil, errors.Wrap(err, "failed to create AWS session")
    43  	}
    44  	return sess, nil
    45  }
    46  
    47  func (b *AmazonBucketProvider) s3() (s3iface.S3API, error) {
    48  	if b.api != nil {
    49  		return b.api, nil
    50  	}
    51  	sess, err := b.createAWSSession()
    52  	if err != nil {
    53  		return nil, errors.Wrap(err, "there was a problem creating the s3 API interface")
    54  	}
    55  	b.api = s3.New(sess)
    56  
    57  	return b.api, nil
    58  }
    59  
    60  func (b *AmazonBucketProvider) s3ManagerDownloader() (s3manageriface.DownloaderAPI, error) {
    61  	if b.downloader != nil {
    62  		return b.downloader, nil
    63  	}
    64  	sess, err := b.createAWSSession()
    65  	if err != nil {
    66  		return nil, errors.Wrap(err, "there was a problem creating the s3ManagerDownloader")
    67  	}
    68  	b.downloader = s3manager.NewDownloader(sess)
    69  	return b.downloader, nil
    70  }
    71  
    72  func (b *AmazonBucketProvider) s3ManagerUploader() (s3manageriface.UploaderAPI, error) {
    73  	if b.uploader != nil {
    74  		return b.uploader, nil
    75  	}
    76  	sess, err := b.createAWSSession()
    77  	if err != nil {
    78  		return nil, errors.Wrap(err, "there was a problem creating the s3ManagerUploader")
    79  	}
    80  	b.uploader = s3manager.NewUploader(sess)
    81  	return b.uploader, nil
    82  }
    83  
    84  // CreateNewBucketForCluster creates a new dynamic bucket
    85  func (b *AmazonBucketProvider) CreateNewBucketForCluster(clusterName string, bucketKind string) (string, error) {
    86  	uuid4 := uuid.New()
    87  	bucketName := fmt.Sprintf("%s-%s-%s", clusterName, bucketKind, uuid4.String())
    88  
    89  	// Max length is 63, https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
    90  	if len(bucketName) > 63 {
    91  		bucketName = bucketName[:63]
    92  	}
    93  	bucketName = strings.TrimRight(bucketName, "-")
    94  	bucketURL := "s3://" + bucketName
    95  	err := b.EnsureBucketIsCreated(bucketURL)
    96  	if err != nil {
    97  		return bucketURL, errors.Wrapf(err, "failed to create bucket %s", bucketURL)
    98  	}
    99  
   100  	return bucketURL, nil
   101  }
   102  
   103  // EnsureBucketIsCreated ensures the bucket URL is created
   104  func (b *AmazonBucketProvider) EnsureBucketIsCreated(bucketURL string) error {
   105  	svc, err := b.s3()
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	u, err := url.Parse(bucketURL)
   111  	if err != nil {
   112  		return errors.Wrapf(err, "failed to parse bucket name from %s", bucketURL)
   113  	}
   114  	bucketName := u.Host
   115  
   116  	// Check if bucket exists already
   117  	_, err = svc.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(bucketName)})
   118  	if err == nil {
   119  		return nil // bucket already exists
   120  	}
   121  	reqFailure, ok := err.(s3.RequestFailure)
   122  	if !ok || reqFailure.StatusCode() != 404 {
   123  		return errors.Wrapf(err, "failed to check if %s bucket exists already", bucketName)
   124  	}
   125  
   126  	infoBucketURL := util.ColorInfo(bucketURL)
   127  	log.Logger().Infof("The bucket %s does not exist so lets create it", infoBucketURL)
   128  
   129  	cbInput := &s3.CreateBucketInput{
   130  		Bucket: aws.String(bucketName),
   131  	}
   132  	// There's a known problem with the S3 API that will make the request fail if you provide a CreateBucketConfiguration
   133  	// with a LocationConstraint pointing to the S3 default us-east-1 region. If not provided, it will be created in that region.
   134  	if b.Requirements.Cluster.Region != "us-east-1" {
   135  		cbInput.CreateBucketConfiguration = &s3.CreateBucketConfiguration{
   136  			LocationConstraint: aws.String(b.Requirements.Cluster.Region),
   137  		}
   138  	}
   139  	_, err = svc.CreateBucket(cbInput)
   140  	if err != nil {
   141  		return errors.Wrapf(err, "there was a problem creating the bucket %s in the AWS", bucketName)
   142  	}
   143  	return nil
   144  }
   145  
   146  // UploadFileToBucket uploads a file to an S3 bucket to the provided bucket with the provided outputName
   147  func (b *AmazonBucketProvider) UploadFileToBucket(reader io.Reader, outputName string, bucketURL string) (string, error) {
   148  	uploader, err := b.s3ManagerUploader()
   149  	if err != nil {
   150  		return "", nil
   151  	}
   152  	bucketURL = strings.TrimPrefix(bucketURL, "s3://")
   153  	output, err := uploader.Upload(&s3manager.UploadInput{
   154  		Bucket:      aws.String(bucketURL),
   155  		Key:         aws.String("/" + outputName),
   156  		ContentType: aws.String(util.ContentTypeForFileName(outputName)),
   157  		Body:        reader,
   158  	})
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  	log.Logger().Debugf("The file was uploaded successfully, location: %s", output.Location)
   163  	return fmt.Sprintf("%s://%s/%s", "s3", bucketURL, outputName), nil
   164  }
   165  
   166  // DownloadFileFromBucket downloads a file from an S3 bucket and converts the contents to a bufio.Scanner
   167  func (b *AmazonBucketProvider) DownloadFileFromBucket(bucketURL string) (io.ReadCloser, error) {
   168  	downloader, err := b.s3ManagerDownloader()
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, "there was a problem downloading from the bucket")
   171  	}
   172  
   173  	u, err := url.Parse(bucketURL)
   174  	if err != nil {
   175  		return nil, errors.Wrapf(err, "the provided bucket location is not a valid URL: %s", bucketURL)
   176  	}
   177  	requestInput := s3.GetObjectInput{
   178  		Bucket: aws.String(u.Host),
   179  		Key:    aws.String(u.Path),
   180  	}
   181  
   182  	f, err := ioutil.TempFile("", ".tmp-s3-download-")
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	defer func() {
   187  		if err != nil {
   188  			_ = f.Close()
   189  			_ = os.Remove(f.Name())
   190  		}
   191  	}()
   192  	_, err = downloader.Download(f, &requestInput)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	return &tempDownloadDestination{f}, nil
   198  }
   199  
   200  type tempDownloadDestination struct {
   201  	*os.File
   202  }
   203  
   204  func (f *tempDownloadDestination) Close() error {
   205  	err1 := f.File.Close()
   206  	err2 := os.Remove(f.File.Name())
   207  	if err2 != nil && err1 == nil {
   208  		err1 = err2
   209  	}
   210  	return err1
   211  }
   212  
   213  // NewAmazonBucketProvider create a new provider for AWS
   214  func NewAmazonBucketProvider(requirements *config.RequirementsConfig) buckets.Provider {
   215  	return &AmazonBucketProvider{
   216  		Requirements: requirements,
   217  	}
   218  }