2018-06-14 05:37:48 +02:00
|
|
|
// Package http implements functionality common to HTTP uploading pipelines.
|
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"io"
|
|
|
|
h "net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/apex/log"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
|
|
"github.com/goreleaser/goreleaser/internal/artifact"
|
2018-08-15 04:50:20 +02:00
|
|
|
"github.com/goreleaser/goreleaser/internal/pipeline"
|
2018-07-10 06:38:00 +02:00
|
|
|
"github.com/goreleaser/goreleaser/internal/semerrgroup"
|
2018-08-15 04:50:20 +02:00
|
|
|
"github.com/goreleaser/goreleaser/pkg/config"
|
|
|
|
"github.com/goreleaser/goreleaser/pkg/context"
|
2018-06-14 05:37:48 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// ModeBinary uploads only compiled binaries
|
|
|
|
ModeBinary = "binary"
|
|
|
|
// ModeArchive uploads release archives
|
|
|
|
ModeArchive = "archive"
|
|
|
|
)
|
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
type asset struct {
|
|
|
|
ReadCloser io.ReadCloser
|
|
|
|
Size int64
|
|
|
|
}
|
|
|
|
|
|
|
|
type assetOpenFunc func(string, *artifact.Artifact) (*asset, error)
|
|
|
|
|
|
|
|
var assetOpen assetOpenFunc
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
assetOpenReset()
|
|
|
|
}
|
|
|
|
|
|
|
|
func assetOpenReset() {
|
|
|
|
assetOpen = assetOpenDefault
|
|
|
|
}
|
|
|
|
|
|
|
|
func assetOpenDefault(kind string, a *artifact.Artifact) (*asset, error) {
|
|
|
|
f, err := os.Open(a.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
s, err := f.Stat()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if s.IsDir() {
|
|
|
|
return nil, errors.Errorf("%s: upload failed: the asset to upload can't be a directory", kind)
|
|
|
|
}
|
|
|
|
return &asset{
|
|
|
|
ReadCloser: f,
|
|
|
|
Size: s.Size(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-06-14 05:37:48 +02:00
|
|
|
// Defaults sets default configuration options on Put structs
|
|
|
|
func Defaults(puts []config.Put) error {
|
|
|
|
for i := range puts {
|
|
|
|
defaults(&puts[i])
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func defaults(put *config.Put) {
|
|
|
|
if put.Mode == "" {
|
|
|
|
put.Mode = ModeArchive
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
// CheckConfig validates a Put configuration returning a descriptive error when appropriate
|
|
|
|
func CheckConfig(ctx *context.Context, put *config.Put, kind string) error {
|
|
|
|
|
|
|
|
if put.Target == "" {
|
|
|
|
return misconfigured(kind, put, "missing target")
|
|
|
|
}
|
2018-06-14 05:37:48 +02:00
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
if put.Name == "" {
|
|
|
|
return misconfigured(kind, put, "missing name")
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
if put.Mode != ModeArchive && put.Mode != ModeBinary {
|
|
|
|
return misconfigured(kind, put, "mode must be 'binary' or 'archive'")
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name))
|
2018-06-14 05:37:48 +02:00
|
|
|
if _, ok := ctx.Env[envName]; !ok {
|
2018-06-25 06:38:11 +02:00
|
|
|
return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName))
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func misconfigured(kind string, upload *config.Put, reason string) error {
|
|
|
|
return pipeline.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason))
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResponseChecker is a function capable of validating an http server response.
|
2018-09-10 14:08:54 +02:00
|
|
|
// It must return and error when the response must be considered a failure.
|
|
|
|
type ResponseChecker func(*h.Response) error
|
2018-06-14 05:37:48 +02:00
|
|
|
|
|
|
|
// Upload does the actual uploading work
|
2018-06-25 06:36:23 +02:00
|
|
|
func Upload(ctx *context.Context, puts []config.Put, kind string, check ResponseChecker) error {
|
2018-06-14 05:37:48 +02:00
|
|
|
if ctx.SkipPublish {
|
|
|
|
return pipeline.ErrSkipPublishEnabled
|
|
|
|
}
|
|
|
|
|
2018-06-25 06:36:23 +02:00
|
|
|
// Handle every configured put
|
|
|
|
for _, put := range puts {
|
|
|
|
filters := []artifact.Filter{}
|
|
|
|
if put.Checksum {
|
|
|
|
filters = append(filters, artifact.ByType(artifact.Checksum))
|
|
|
|
}
|
|
|
|
if put.Signature {
|
|
|
|
filters = append(filters, artifact.ByType(artifact.Signature))
|
|
|
|
}
|
2018-06-14 05:37:48 +02:00
|
|
|
// We support two different modes
|
|
|
|
// - "archive": Upload all artifacts
|
|
|
|
// - "binary": Upload only the raw binaries
|
2018-06-25 06:36:23 +02:00
|
|
|
switch v := strings.ToLower(put.Mode); v {
|
2018-06-14 05:37:48 +02:00
|
|
|
case ModeArchive:
|
2018-06-25 06:36:23 +02:00
|
|
|
filters = append(filters,
|
2018-06-14 05:37:48 +02:00
|
|
|
artifact.ByType(artifact.UploadableArchive),
|
2018-06-25 06:36:23 +02:00
|
|
|
artifact.ByType(artifact.LinuxPackage))
|
2018-06-14 05:37:48 +02:00
|
|
|
case ModeBinary:
|
2018-06-25 06:36:23 +02:00
|
|
|
filters = append(filters,
|
|
|
|
artifact.ByType(artifact.UploadableBinary))
|
2018-06-14 05:37:48 +02:00
|
|
|
default:
|
|
|
|
err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v)
|
|
|
|
log.WithFields(log.Fields{
|
2018-06-25 06:36:23 +02:00
|
|
|
kind: put.Name,
|
|
|
|
"mode": v,
|
2018-06-14 05:37:48 +02:00
|
|
|
}).Error(err.Error())
|
|
|
|
return err
|
|
|
|
}
|
2018-08-15 05:26:57 +02:00
|
|
|
if err := uploadWithFilter(ctx, put, artifact.Or(filters...), kind, check); err != nil {
|
2018-06-14 05:37:48 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-15 05:26:57 +02:00
|
|
|
func uploadWithFilter(ctx *context.Context, put config.Put, filter artifact.Filter, kind string, check ResponseChecker) error {
|
2018-07-10 06:38:00 +02:00
|
|
|
var g = semerrgroup.New(ctx.Parallelism)
|
2018-06-14 05:37:48 +02:00
|
|
|
for _, artifact := range ctx.Artifacts.Filter(filter).List() {
|
|
|
|
artifact := artifact
|
|
|
|
g.Go(func() error {
|
2018-06-25 06:38:11 +02:00
|
|
|
return uploadAsset(ctx, put, artifact, kind, check)
|
2018-06-14 05:37:48 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
return g.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
// uploadAsset uploads file to target and logs all actions
|
2018-06-25 06:38:11 +02:00
|
|
|
func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error {
|
2018-08-25 22:26:06 +02:00
|
|
|
envBase := fmt.Sprintf("%s_%s_", strings.ToUpper(kind), strings.ToUpper(put.Name))
|
|
|
|
username := put.Username
|
|
|
|
if username == "" {
|
|
|
|
// username not configured: using env
|
|
|
|
username = ctx.Env[envBase+"USERNAME"]
|
|
|
|
}
|
|
|
|
secret := ctx.Env[envBase+"SECRET"]
|
2018-06-14 05:37:48 +02:00
|
|
|
|
|
|
|
// Generate the target url
|
2018-06-25 06:38:11 +02:00
|
|
|
targetURL, err := resolveTargetTemplate(ctx, put, artifact)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
|
|
|
msg := fmt.Sprintf("%s: error while building the target url", kind)
|
2018-06-25 06:38:11 +02:00
|
|
|
log.WithField("instance", put.Name).WithError(err).Error(msg)
|
2018-06-14 05:37:48 +02:00
|
|
|
return errors.Wrap(err, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle the artifact
|
2018-06-25 06:38:11 +02:00
|
|
|
asset, err := assetOpen(kind, &artifact)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-06-25 06:38:11 +02:00
|
|
|
defer asset.ReadCloser.Close() // nolint: errcheck
|
2018-06-14 05:37:48 +02:00
|
|
|
|
|
|
|
// The target url needs to contain the artifact name
|
|
|
|
if !strings.HasSuffix(targetURL, "/") {
|
|
|
|
targetURL += "/"
|
|
|
|
}
|
|
|
|
targetURL += artifact.Name
|
|
|
|
|
2018-09-10 14:08:54 +02:00
|
|
|
_, err = uploadAssetToServer(ctx, targetURL, username, secret, asset, check)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
|
|
|
msg := fmt.Sprintf("%s: upload failed", kind)
|
|
|
|
log.WithError(err).WithFields(log.Fields{
|
2018-06-25 06:38:11 +02:00
|
|
|
"instance": put.Name,
|
2018-08-25 22:26:06 +02:00
|
|
|
"username": username,
|
2018-06-14 05:37:48 +02:00
|
|
|
}).Error(msg)
|
|
|
|
return errors.Wrap(err, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.WithFields(log.Fields{
|
2018-06-25 06:38:11 +02:00
|
|
|
"instance": put.Name,
|
|
|
|
"mode": put.Mode,
|
2018-06-14 05:37:48 +02:00
|
|
|
}).Info("uploaded successful")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// uploadAssetToServer uploads the asset file to target
|
2018-09-10 14:08:54 +02:00
|
|
|
func uploadAssetToServer(ctx *context.Context, target, username, secret string, a *asset, check ResponseChecker) (*h.Response, error) {
|
2018-06-25 06:38:11 +02:00
|
|
|
req, err := newUploadRequest(target, username, secret, a)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
2018-09-10 14:08:54 +02:00
|
|
|
return nil, err
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 14:08:54 +02:00
|
|
|
return executeHTTPRequest(ctx, req, check)
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// newUploadRequest creates a new h.Request for uploading
|
2018-06-25 06:38:11 +02:00
|
|
|
func newUploadRequest(target, username, secret string, a *asset) (*h.Request, error) {
|
2018-06-14 05:37:48 +02:00
|
|
|
u, err := url.Parse(target)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-25 06:38:11 +02:00
|
|
|
req, err := h.NewRequest("PUT", u.String(), a.ReadCloser)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-06-25 06:38:11 +02:00
|
|
|
req.ContentLength = a.Size
|
2018-06-14 05:37:48 +02:00
|
|
|
req.SetBasicAuth(username, secret)
|
|
|
|
|
|
|
|
return req, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// executeHTTPRequest processes the http call with respect of context ctx
|
2018-09-10 14:08:54 +02:00
|
|
|
func executeHTTPRequest(ctx *context.Context, req *h.Request, check ResponseChecker) (*h.Response, error) {
|
2018-06-14 05:37:48 +02:00
|
|
|
resp, err := h.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
// If we got an error, and the context has been canceled,
|
|
|
|
// the context's error is probably more useful.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2018-09-10 14:08:54 +02:00
|
|
|
return nil, ctx.Err()
|
2018-06-14 05:37:48 +02:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2018-09-10 14:08:54 +02:00
|
|
|
return nil, err
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close() // nolint: errcheck
|
|
|
|
|
2018-09-10 14:08:54 +02:00
|
|
|
err = check(resp)
|
2018-06-14 05:37:48 +02:00
|
|
|
if err != nil {
|
|
|
|
// even though there was an error, we still return the response
|
|
|
|
// in case the caller wants to inspect it further
|
2018-09-10 14:08:54 +02:00
|
|
|
return resp, err
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 14:08:54 +02:00
|
|
|
return resp, err
|
2018-06-14 05:37:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// targetData is used as a template struct for
|
|
|
|
// Artifactory.Target
|
|
|
|
type targetData struct {
|
|
|
|
Version string
|
|
|
|
Tag string
|
|
|
|
ProjectName string
|
|
|
|
|
|
|
|
// Only supported in mode binary
|
|
|
|
Os string
|
|
|
|
Arch string
|
|
|
|
Arm string
|
|
|
|
}
|
|
|
|
|
|
|
|
// resolveTargetTemplate returns the resolved target template with replaced variables
|
|
|
|
// Those variables can be replaced by the given context, goos, goarch, goarm and more
|
|
|
|
func resolveTargetTemplate(ctx *context.Context, artifactory config.Put, artifact artifact.Artifact) (string, error) {
|
|
|
|
data := targetData{
|
|
|
|
Version: ctx.Version,
|
|
|
|
Tag: ctx.Git.CurrentTag,
|
|
|
|
ProjectName: ctx.Config.ProjectName,
|
|
|
|
}
|
|
|
|
|
|
|
|
if artifactory.Mode == ModeBinary {
|
|
|
|
data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos)
|
|
|
|
data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch)
|
|
|
|
data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm)
|
|
|
|
}
|
|
|
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
err = t.Execute(&out, data)
|
|
|
|
return out.String(), err
|
|
|
|
}
|
|
|
|
|
|
|
|
func replace(replacements map[string]string, original string) string {
|
|
|
|
result := replacements[original]
|
|
|
|
if result == "" {
|
|
|
|
return original
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|