1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-26 04:22:05 +02:00
Pablo Lalloni 3b0f3f55c8 feat: support simple http put artifacts uploading
Supports doing simple http put requests for uploading artifacts.
Heavily based con Artifactory pipeline.
In archive mode uploads all archives and the checksums file.
In binary mode uploads just the binaries.
2018-06-25 10:01:31 -03:00

320 lines
8.1 KiB
Go

package httpupload
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/alecthomas/template"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/config"
"github.com/goreleaser/goreleaser/context"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/pipeline"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
const (
modeBinary = "binary"
modeArchive = "archive"
)
type httpServerResponse struct {
Created bool
}
// Pipe for http publishing
type Pipe struct{}
// String returns the description of the pipe
func (Pipe) String() string {
return "releasing to HTTP"
}
// Default sets the pipe defaults
func (Pipe) Default(ctx *context.Context) error {
if len(ctx.Config.HTTPUploads) == 0 {
return nil
}
// Check if a mode was set
for i := range ctx.Config.HTTPUploads {
if ctx.Config.HTTPUploads[i].Mode == "" {
ctx.Config.HTTPUploads[i].Mode = modeArchive
}
}
return nil
}
// Run the pipe
func (Pipe) Run(ctx *context.Context) error {
if len(ctx.Config.HTTPUploads) == 0 {
return pipeline.Skip("http uploads section is not configured")
}
// Check requirements for every instance we have configured.
// If not fulfilled, we can skip this pipeline
for _, instance := range ctx.Config.HTTPUploads {
if instance.Target == "" {
return pipeline.Skip("http upload section is not configured properly (missing target)")
}
if instance.Username == "" {
return pipeline.Skip("http upload section is not configured properly (missing username)")
}
if instance.Name == "" {
return pipeline.Skip("http upload section is not configured properly (missing name)")
}
envName := fmt.Sprintf("HTTP_UPLOAD_%s_SECRET", strings.ToUpper(instance.Name))
if _, ok := ctx.Env[envName]; !ok {
return pipeline.Skip(fmt.Sprintf("missing secret for http upload instance %s", instance.Name))
}
}
return doRun(ctx)
}
func doRun(ctx *context.Context) error {
if ctx.SkipPublish {
return pipeline.ErrSkipPublishEnabled
}
// Handle every configured http upload instance
for _, instance := range ctx.Config.HTTPUploads {
// We support two different modes
// - "archive": Upload all artifacts
// - "binary": Upload only the raw binaries
var filter artifact.Filter
switch v := strings.ToLower(instance.Mode); v {
case modeArchive:
filter = artifact.Or(
artifact.ByType(artifact.UploadableArchive),
artifact.ByType(artifact.LinuxPackage),
artifact.ByType(artifact.Checksum),
artifact.ByType(artifact.Signature),
)
case modeBinary:
filter = artifact.ByType(artifact.UploadableBinary)
default:
err := fmt.Errorf("httpupload: mode \"%s\" not supported", v)
log.WithFields(log.Fields{
"instance": instance.Name,
"mode": v,
}).Error(err.Error())
return err
}
if err := runPipeByFilter(ctx, instance, filter); err != nil {
return err
}
}
return nil
}
func runPipeByFilter(ctx *context.Context, instance config.HTTPUpload, filter artifact.Filter) error {
sem := make(chan bool, ctx.Parallelism)
var g errgroup.Group
for _, artifact := range ctx.Artifacts.Filter(filter).List() {
sem <- true
artifact := artifact
g.Go(func() error {
defer func() {
<-sem
}()
return uploadAsset(ctx, instance, artifact)
})
}
return g.Wait()
}
func uploadAsset(ctx *context.Context, instance config.HTTPUpload, artifact artifact.Artifact) error {
envName := fmt.Sprintf("HTTP_UPLOAD_%s_SECRET", strings.ToUpper(instance.Name))
secret := ctx.Env[envName]
// Generate the target url
targetURL, err := resolveTargetTemplate(ctx, instance, artifact)
if err != nil {
msg := "httpupload: error while building the target url"
log.WithField("instance", instance.Name).WithError(err).Error(msg)
return errors.Wrap(err, msg)
}
// Handle the artifact
file, err := os.Open(artifact.Path)
if err != nil {
return err
}
defer file.Close() // nolint: errcheck
// The target url needs to contain the artifact name
if !strings.HasSuffix(targetURL, "/") {
targetURL += "/"
}
targetURL += artifact.Name
_, _, err = uploadAssetToHTTPServer(ctx, targetURL, instance.Username, secret, file)
if err != nil {
msg := "httpupload: upload failed"
log.WithError(err).WithFields(log.Fields{
"instance": instance.Name,
"username": instance.Username,
}).Error(msg)
return errors.Wrap(err, msg)
}
log.WithFields(log.Fields{
"instance": instance.Name,
"mode": instance.Mode,
"uri": targetURL,
}).Info("uploaded successful")
return nil
}
// targetData is used as a template struct for HTTPUpload.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, upload config.HTTPUpload, artifact artifact.Artifact) (string, error) {
data := targetData{
Version: ctx.Version,
Tag: ctx.Git.CurrentTag,
ProjectName: ctx.Config.ProjectName,
}
if upload.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(upload.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
}
// uploadAssetToHTTPServer uploads the asset file to target
func uploadAssetToHTTPServer(ctx *context.Context, target, username, secret string, file *os.File) (*httpServerResponse, *http.Response, error) {
stat, err := file.Stat()
if err != nil {
return nil, nil, err
}
if stat.IsDir() {
return nil, nil, errors.New("the asset to upload can't be a directory")
}
req, err := newUploadRequest(target, username, secret, file, stat.Size())
if err != nil {
return nil, nil, err
}
asset := new(httpServerResponse)
resp, err := executeHTTPRequest(ctx, req, asset)
if err != nil {
return nil, resp, err
}
return asset, resp, nil
}
// newUploadRequest creates a new http.Request for uploading
func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) {
u, err := url.Parse(target)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PUT", u.String(), reader)
if err != nil {
return nil, err
}
req.ContentLength = size
req.SetBasicAuth(username, secret)
return req, err
}
// executeHTTPRequest processes the http call with respect of context ctx
func executeHTTPRequest(ctx *context.Context, req *http.Request, v *httpServerResponse) (*http.Response, error) {
resp, err := http.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():
return nil, ctx.Err()
default:
}
return nil, err
}
defer resp.Body.Close() // nolint: errcheck
err = checkResponse(resp)
if err != nil {
// even though there was an error, we still return the response
// in case the caller wants to inspect it further
return resp, err
}
v.Created = resp.StatusCode == http.StatusCreated
return resp, err
}
// An ErrorResponse reports one or more errors caused by an API request.
type errorResponse struct {
Response *http.Response // HTTP response that caused this error
}
func (r *errorResponse) Error() string {
return fmt.Sprintf("%v %v: %s",
r.Response.Request.Method,
r.Response.Request.URL,
r.Response.Status)
}
// checkResponse checks the HTTP response for errors, and returns them if
// present. A response is considered an error if it has a status code outside
// the 200 range.
// API error responses are expected to have no response body.
func checkResponse(r *http.Response) error {
if c := r.StatusCode; 200 <= c && c <= 299 {
return nil
}
return &errorResponse{Response: r}
}