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 }