github.com/tahsinrahman/goreleaser@v0.79.1/internal/http/http.go (about) 1 // Package http implements functionality common to HTTP uploading pipelines. 2 package http 3 4 import ( 5 "bytes" 6 "fmt" 7 "html/template" 8 "io" 9 h "net/http" 10 "net/url" 11 "os" 12 "strings" 13 14 "github.com/apex/log" 15 "github.com/pkg/errors" 16 "golang.org/x/sync/errgroup" 17 18 "github.com/goreleaser/goreleaser/config" 19 "github.com/goreleaser/goreleaser/context" 20 "github.com/goreleaser/goreleaser/internal/artifact" 21 "github.com/goreleaser/goreleaser/pipeline" 22 ) 23 24 const ( 25 // ModeBinary uploads only compiled binaries 26 ModeBinary = "binary" 27 // ModeArchive uploads release archives 28 ModeArchive = "archive" 29 ) 30 31 type asset struct { 32 ReadCloser io.ReadCloser 33 Size int64 34 } 35 36 type assetOpenFunc func(string, *artifact.Artifact) (*asset, error) 37 38 var assetOpen assetOpenFunc 39 40 func init() { 41 assetOpenReset() 42 } 43 44 func assetOpenReset() { 45 assetOpen = assetOpenDefault 46 } 47 48 func assetOpenDefault(kind string, a *artifact.Artifact) (*asset, error) { 49 f, err := os.Open(a.Path) 50 if err != nil { 51 return nil, err 52 } 53 s, err := f.Stat() 54 if err != nil { 55 return nil, err 56 } 57 if s.IsDir() { 58 return nil, errors.Errorf("%s: upload failed: the asset to upload can't be a directory", kind) 59 } 60 return &asset{ 61 ReadCloser: f, 62 Size: s.Size(), 63 }, nil 64 } 65 66 // Defaults sets default configuration options on Put structs 67 func Defaults(puts []config.Put) error { 68 for i := range puts { 69 defaults(&puts[i]) 70 } 71 return nil 72 } 73 74 func defaults(put *config.Put) { 75 if put.Mode == "" { 76 put.Mode = ModeArchive 77 } 78 } 79 80 // CheckConfig validates a Put configuration returning a descriptive error when appropriate 81 func CheckConfig(ctx *context.Context, put *config.Put, kind string) error { 82 83 if put.Target == "" { 84 return misconfigured(kind, put, "missing target") 85 } 86 87 if put.Username == "" { 88 return misconfigured(kind, put, "missing username") 89 } 90 91 if put.Name == "" { 92 return misconfigured(kind, put, "missing name") 93 } 94 95 if put.Mode != ModeArchive && put.Mode != ModeBinary { 96 return misconfigured(kind, put, "mode must be 'binary' or 'archive'") 97 } 98 99 envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name)) 100 if _, ok := ctx.Env[envName]; !ok { 101 return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName)) 102 } 103 104 return nil 105 106 } 107 108 func misconfigured(kind string, upload *config.Put, reason string) error { 109 return pipeline.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason)) 110 } 111 112 // ResponseChecker is a function capable of validating an http server response. 113 // It must return the location of the uploaded asset or the error when the 114 // response must be considered a failure. 115 type ResponseChecker func(*h.Response) (string, error) 116 117 // Upload does the actual uploading work 118 func Upload(ctx *context.Context, puts []config.Put, kind string, check ResponseChecker) error { 119 if ctx.SkipPublish { 120 return pipeline.ErrSkipPublishEnabled 121 } 122 123 // Handle every configured put 124 for _, put := range puts { 125 filters := []artifact.Filter{} 126 if put.Checksum { 127 filters = append(filters, artifact.ByType(artifact.Checksum)) 128 } 129 if put.Signature { 130 filters = append(filters, artifact.ByType(artifact.Signature)) 131 } 132 // We support two different modes 133 // - "archive": Upload all artifacts 134 // - "binary": Upload only the raw binaries 135 switch v := strings.ToLower(put.Mode); v { 136 case ModeArchive: 137 filters = append(filters, 138 artifact.ByType(artifact.UploadableArchive), 139 artifact.ByType(artifact.LinuxPackage)) 140 case ModeBinary: 141 filters = append(filters, 142 artifact.ByType(artifact.UploadableBinary)) 143 default: 144 err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v) 145 log.WithFields(log.Fields{ 146 kind: put.Name, 147 "mode": v, 148 }).Error(err.Error()) 149 return err 150 } 151 if err := runPipeByFilter(ctx, put, artifact.Or(filters...), kind, check); err != nil { 152 return err 153 } 154 } 155 156 return nil 157 } 158 159 func runPipeByFilter(ctx *context.Context, put config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { 160 sem := make(chan bool, ctx.Parallelism) 161 var g errgroup.Group 162 for _, artifact := range ctx.Artifacts.Filter(filter).List() { 163 sem <- true 164 artifact := artifact 165 g.Go(func() error { 166 defer func() { 167 <-sem 168 }() 169 return uploadAsset(ctx, put, artifact, kind, check) 170 }) 171 } 172 return g.Wait() 173 } 174 175 // uploadAsset uploads file to target and logs all actions 176 func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { 177 envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name)) 178 secret := ctx.Env[envName] 179 180 // Generate the target url 181 targetURL, err := resolveTargetTemplate(ctx, put, artifact) 182 if err != nil { 183 msg := fmt.Sprintf("%s: error while building the target url", kind) 184 log.WithField("instance", put.Name).WithError(err).Error(msg) 185 return errors.Wrap(err, msg) 186 } 187 188 // Handle the artifact 189 asset, err := assetOpen(kind, &artifact) 190 if err != nil { 191 return err 192 } 193 defer asset.ReadCloser.Close() // nolint: errcheck 194 195 // The target url needs to contain the artifact name 196 if !strings.HasSuffix(targetURL, "/") { 197 targetURL += "/" 198 } 199 targetURL += artifact.Name 200 201 location, _, err := uploadAssetToServer(ctx, targetURL, put.Username, secret, asset, check) 202 if err != nil { 203 msg := fmt.Sprintf("%s: upload failed", kind) 204 log.WithError(err).WithFields(log.Fields{ 205 "instance": put.Name, 206 "username": put.Username, 207 }).Error(msg) 208 return errors.Wrap(err, msg) 209 } 210 211 log.WithFields(log.Fields{ 212 "instance": put.Name, 213 "mode": put.Mode, 214 "uri": location, 215 }).Info("uploaded successful") 216 217 return nil 218 } 219 220 // uploadAssetToServer uploads the asset file to target 221 func uploadAssetToServer(ctx *context.Context, target, username, secret string, a *asset, check ResponseChecker) (string, *h.Response, error) { 222 req, err := newUploadRequest(target, username, secret, a) 223 if err != nil { 224 return "", nil, err 225 } 226 227 loc, resp, err := executeHTTPRequest(ctx, req, check) 228 if err != nil { 229 return "", resp, err 230 } 231 return loc, resp, nil 232 } 233 234 // newUploadRequest creates a new h.Request for uploading 235 func newUploadRequest(target, username, secret string, a *asset) (*h.Request, error) { 236 u, err := url.Parse(target) 237 if err != nil { 238 return nil, err 239 } 240 req, err := h.NewRequest("PUT", u.String(), a.ReadCloser) 241 if err != nil { 242 return nil, err 243 } 244 245 req.ContentLength = a.Size 246 req.SetBasicAuth(username, secret) 247 248 return req, err 249 } 250 251 // executeHTTPRequest processes the http call with respect of context ctx 252 func executeHTTPRequest(ctx *context.Context, req *h.Request, check ResponseChecker) (string, *h.Response, error) { 253 resp, err := h.DefaultClient.Do(req) 254 if err != nil { 255 // If we got an error, and the context has been canceled, 256 // the context's error is probably more useful. 257 select { 258 case <-ctx.Done(): 259 return "", nil, ctx.Err() 260 default: 261 } 262 263 return "", nil, err 264 } 265 266 defer resp.Body.Close() // nolint: errcheck 267 268 loc, err := check(resp) 269 if err != nil { 270 // even though there was an error, we still return the response 271 // in case the caller wants to inspect it further 272 return "", resp, err 273 } 274 275 return loc, resp, err 276 } 277 278 // targetData is used as a template struct for 279 // Artifactory.Target 280 type targetData struct { 281 Version string 282 Tag string 283 ProjectName string 284 285 // Only supported in mode binary 286 Os string 287 Arch string 288 Arm string 289 } 290 291 // resolveTargetTemplate returns the resolved target template with replaced variables 292 // Those variables can be replaced by the given context, goos, goarch, goarm and more 293 func resolveTargetTemplate(ctx *context.Context, artifactory config.Put, artifact artifact.Artifact) (string, error) { 294 data := targetData{ 295 Version: ctx.Version, 296 Tag: ctx.Git.CurrentTag, 297 ProjectName: ctx.Config.ProjectName, 298 } 299 300 if artifactory.Mode == ModeBinary { 301 data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos) 302 data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch) 303 data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm) 304 } 305 306 var out bytes.Buffer 307 t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target) 308 if err != nil { 309 return "", err 310 } 311 err = t.Execute(&out, data) 312 return out.String(), err 313 } 314 315 func replace(replacements map[string]string, original string) string { 316 result := replacements[original] 317 if result == "" { 318 return original 319 } 320 return result 321 }