mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-26 04:22:05 +02:00
3b0f3f55c8
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.
320 lines
8.1 KiB
Go
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}
|
|
}
|