github.com/triarius/goreleaser@v1.12.5/internal/pipe/blob/upload.go (about)

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