github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/internal/pipe/blob/upload.go (about)

     1  package blob
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/url"
     8  	"path"
     9  
    10  	"github.com/apex/log"
    11  	"github.com/goreleaser/goreleaser/internal/artifact"
    12  	"github.com/goreleaser/goreleaser/internal/extrafiles"
    13  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    14  	"github.com/goreleaser/goreleaser/internal/tmpl"
    15  	"github.com/goreleaser/goreleaser/pkg/config"
    16  	"github.com/goreleaser/goreleaser/pkg/context"
    17  	"gocloud.dev/blob"
    18  	"gocloud.dev/secrets"
    19  
    20  	// Import the blob packages we want to be able to open.
    21  	_ "gocloud.dev/blob/azureblob"
    22  	_ "gocloud.dev/blob/gcsblob"
    23  	_ "gocloud.dev/blob/s3blob"
    24  
    25  	// import the secrets packages we want to be able to open:.
    26  	_ "gocloud.dev/secrets/awskms"
    27  	_ "gocloud.dev/secrets/azurekeyvault"
    28  	_ "gocloud.dev/secrets/gcpkms"
    29  )
    30  
    31  func urlFor(ctx *context.Context, conf config.Blob) (string, error) {
    32  	bucket, err := tmpl.New(ctx).Apply(conf.Bucket)
    33  	if err != nil {
    34  		return "", err
    35  	}
    36  
    37  	bucketURL := fmt.Sprintf("%s://%s", conf.Provider, bucket)
    38  
    39  	if conf.Provider != "s3" {
    40  		return bucketURL, nil
    41  	}
    42  
    43  	var query = url.Values{}
    44  	if conf.Endpoint != "" {
    45  		query.Add("endpoint", conf.Endpoint)
    46  		query.Add("s3ForcePathStyle", "true")
    47  	}
    48  	if conf.Region != "" {
    49  		query.Add("region", conf.Region)
    50  	}
    51  	if conf.DisableSSL {
    52  		query.Add("disableSSL", "true")
    53  	}
    54  
    55  	if len(query) > 0 {
    56  		bucketURL = bucketURL + "?" + query.Encode()
    57  	}
    58  
    59  	return bucketURL, nil
    60  }
    61  
    62  // Takes goreleaser context(which includes artificats) and bucketURL for
    63  // upload to destination (eg: gs://gorelease-bucket) using the given uploader
    64  // implementation.
    65  func doUpload(ctx *context.Context, conf config.Blob) error {
    66  	folder, err := tmpl.New(ctx).Apply(conf.Folder)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	bucketURL, err := urlFor(ctx, conf)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	var filter = artifact.Or(
    77  		artifact.ByType(artifact.UploadableArchive),
    78  		artifact.ByType(artifact.UploadableBinary),
    79  		artifact.ByType(artifact.UploadableSourceArchive),
    80  		artifact.ByType(artifact.Checksum),
    81  		artifact.ByType(artifact.Signature),
    82  		artifact.ByType(artifact.LinuxPackage),
    83  	)
    84  	if len(conf.IDs) > 0 {
    85  		filter = artifact.And(filter, artifact.ByIDs(conf.IDs...))
    86  	}
    87  
    88  	var up = newUploader(ctx)
    89  	if err := up.Open(ctx, bucketURL); err != nil {
    90  		return handleError(err, bucketURL)
    91  	}
    92  	defer up.Close()
    93  
    94  	var g = semerrgroup.New(ctx.Parallelism)
    95  	for _, artifact := range ctx.Artifacts.Filter(filter).List() {
    96  		artifact := artifact
    97  		g.Go(func() error {
    98  			// TODO: replace this with ?prefix=folder on the bucket url
    99  			var dataFile = artifact.Path
   100  			var uploadFile = path.Join(folder, artifact.Name)
   101  
   102  			err := uploadData(ctx, conf, up, dataFile, uploadFile, bucketURL)
   103  
   104  			return err
   105  		})
   106  	}
   107  
   108  	files, err := extrafiles.Find(conf.ExtraFiles)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	for name, fullpath := range files {
   113  		name := name
   114  		fullpath := fullpath
   115  		g.Go(func() error {
   116  			var uploadFile = path.Join(folder, name)
   117  
   118  			err := uploadData(ctx, conf, up, fullpath, uploadFile, bucketURL)
   119  
   120  			return err
   121  		})
   122  	}
   123  
   124  	return g.Wait()
   125  }
   126  
   127  func uploadData(ctx *context.Context, conf config.Blob, up uploader, dataFile, uploadFile, bucketURL string) error {
   128  	data, err := getData(ctx, conf, dataFile)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	err = up.Upload(ctx, uploadFile, data)
   134  	if err != nil {
   135  		return handleError(err, bucketURL)
   136  	}
   137  	return err
   138  }
   139  
   140  func handleError(err error, url string) error {
   141  	switch {
   142  	case errorContains(err, "NoSuchBucket", "ContainerNotFound", "notFound"):
   143  		return fmt.Errorf("provided bucket does not exist: %s: %w", url, err)
   144  	case errorContains(err, "NoCredentialProviders"):
   145  		return fmt.Errorf("check credentials and access to bucket: %s: %w", url, err)
   146  	case errorContains(err, "InvalidAccessKeyId"):
   147  		return fmt.Errorf("aws access key id you provided does not exist in our records: %w", err)
   148  	case errorContains(err, "AuthenticationFailed"):
   149  		return fmt.Errorf("azure storage key you provided is not valid: %w", err)
   150  	case errorContains(err, "invalid_grant"):
   151  		return fmt.Errorf("google app credentials you provided is not valid: %w", err)
   152  	case errorContains(err, "no such host"):
   153  		return fmt.Errorf("azure storage account you provided is not valid: %w", err)
   154  	case errorContains(err, "ServiceCode=ResourceNotFound"):
   155  		return fmt.Errorf("missing azure storage key for provided bucket %s: %w", url, err)
   156  	default:
   157  		return fmt.Errorf("failed to write to bucket: %w", err)
   158  	}
   159  }
   160  
   161  func newUploader(ctx *context.Context) uploader {
   162  	if ctx.SkipPublish {
   163  		return &skipUploader{}
   164  	}
   165  	return &productionUploader{}
   166  }
   167  
   168  func getData(ctx *context.Context, conf config.Blob, path string) ([]byte, error) {
   169  	data, err := ioutil.ReadFile(path)
   170  	if err != nil {
   171  		return data, fmt.Errorf("failed to open file %s: %w", path, err)
   172  	}
   173  	if conf.KMSKey == "" {
   174  		return data, nil
   175  	}
   176  	keeper, err := secrets.OpenKeeper(ctx, conf.KMSKey)
   177  	if err != nil {
   178  		return data, fmt.Errorf("failed to open kms %s: %w", conf.KMSKey, err)
   179  	}
   180  	defer keeper.Close()
   181  	data, err = keeper.Encrypt(ctx, data)
   182  	if err != nil {
   183  		return data, fmt.Errorf("failed to encrypt with kms: %w", err)
   184  	}
   185  	return data, err
   186  }
   187  
   188  // uploader implements upload.
   189  type uploader interface {
   190  	io.Closer
   191  	Open(ctx *context.Context, url string) error
   192  	Upload(ctx *context.Context, path string, data []byte) error
   193  }
   194  
   195  // skipUploader is used when --skip-upload is set and will just log
   196  // things without really doing anything.
   197  type skipUploader struct{}
   198  
   199  func (u *skipUploader) Close() error                            { return nil }
   200  func (u *skipUploader) Open(_ *context.Context, _ string) error { return nil }
   201  
   202  func (u *skipUploader) Upload(_ *context.Context, path string, _ []byte) error {
   203  	log.WithField("path", path).Warn("upload skipped because skip-publish is set")
   204  	return nil
   205  }
   206  
   207  // productionUploader actually do upload to.
   208  type productionUploader struct {
   209  	bucket *blob.Bucket
   210  }
   211  
   212  func (u *productionUploader) Close() error {
   213  	if u.bucket == nil {
   214  		return nil
   215  	}
   216  	return u.bucket.Close()
   217  }
   218  func (u *productionUploader) Open(ctx *context.Context, bucket string) error {
   219  	log.WithFields(log.Fields{
   220  		"bucket": bucket,
   221  	}).Debug("uploading")
   222  
   223  	conn, err := blob.OpenBucket(ctx, bucket)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	u.bucket = conn
   228  	return nil
   229  }
   230  
   231  func (u *productionUploader) Upload(ctx *context.Context, filepath string, data []byte) (err error) {
   232  	log.WithField("path", filepath).Info("uploading")
   233  
   234  	opts := &blob.WriterOptions{
   235  		ContentDisposition: "attachment; filename=" + path.Base(filepath),
   236  	}
   237  	w, err := u.bucket.NewWriter(ctx, filepath, opts)
   238  	if err != nil {
   239  		return err
   240  	}
   241  	defer func() {
   242  		if cerr := w.Close(); err == nil {
   243  			err = cerr
   244  		}
   245  	}()
   246  	_, err = w.Write(data)
   247  	return
   248  }