mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-10 03:47:03 +02:00
4021a8e41f
uploadBinary and uploadArchive where nearly the same code. We generalized uploadAssetAndLog() to handle both cases
430 lines
12 KiB
Go
430 lines
12 KiB
Go
// Package artifactory provides a Pipe that push to artifactory
|
|
package artifactory
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/goreleaser/goreleaser/config"
|
|
"github.com/goreleaser/goreleaser/context"
|
|
"github.com/goreleaser/goreleaser/internal/buildtarget"
|
|
"github.com/goreleaser/goreleaser/pipeline"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/apex/log"
|
|
)
|
|
|
|
// artifactoryResponse reflects the response after an upload request
|
|
// to Artifactory.
|
|
type artifactoryResponse struct {
|
|
Repo string `json:"repo,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Created string `json:"created,omitempty"`
|
|
CreatedBy string `json:"createdBy,omitempty"`
|
|
DownloadURI string `json:"downloadUri,omitempty"`
|
|
MimeType string `json:"mimeType,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
Checksums artifactoryChecksums `json:"checksums,omitempty"`
|
|
OriginalChecksums artifactoryChecksums `json:"originalChecksums,omitempty"`
|
|
URI string `json:"uri,omitempty"`
|
|
}
|
|
|
|
// artifactoryChecksums reflects the checksums generated by
|
|
// Artifactory
|
|
type artifactoryChecksums struct {
|
|
SHA1 string `json:"sha1,omitempty"`
|
|
MD5 string `json:"md5,omitempty"`
|
|
SHA256 string `json:"sha256,omitempty"`
|
|
}
|
|
|
|
const (
|
|
modeBinary = "binary"
|
|
modeArchive = "archive"
|
|
)
|
|
|
|
// Pipe for Artifactory
|
|
type Pipe struct{}
|
|
|
|
// String returns the description of the pipe
|
|
func (Pipe) String() string {
|
|
return "releasing to Artifactory"
|
|
}
|
|
|
|
// Default sets the pipe defaults
|
|
func (Pipe) Default(ctx *context.Context) error {
|
|
if l := len(ctx.Config.Artifactories); l == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Check if a mode was set
|
|
for i := range ctx.Config.Artifactories {
|
|
if ctx.Config.Artifactories[i].Mode == "" {
|
|
ctx.Config.Artifactories[i].Mode = "archive"
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Run the pipe
|
|
//
|
|
// Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact
|
|
func (Pipe) Run(ctx *context.Context) error {
|
|
if l := len(ctx.Config.Artifactories); l == 0 {
|
|
return pipeline.Skip("artifactory 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.Artifactories {
|
|
if instance.Target == "" {
|
|
return pipeline.Skip("artifactory section is not configured properly (missing target)")
|
|
}
|
|
|
|
if instance.Username == "" {
|
|
return pipeline.Skip("artifactory section is not configured properly (missing username)")
|
|
}
|
|
|
|
if instance.Name == "" {
|
|
return pipeline.Skip("artifactory section is not configured properly (missing name)")
|
|
}
|
|
|
|
envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name))
|
|
if os.Getenv(envName) == "" {
|
|
return pipeline.Skip(fmt.Sprintf("missing secret for artifactory instance %s", instance.Name))
|
|
}
|
|
}
|
|
|
|
return doRun(ctx)
|
|
}
|
|
|
|
func doRun(ctx *context.Context) error {
|
|
if !ctx.Publish {
|
|
return pipeline.Skip("--skip-publish is set")
|
|
}
|
|
|
|
// Handle every configured artifactory instance
|
|
for _, instance := range ctx.Config.Artifactories {
|
|
// We support two different modes
|
|
// - "archive": Upload all artifacts
|
|
// - "binary": Upload only the raw binaries
|
|
var err error
|
|
switch v := strings.ToLower(instance.Mode); v {
|
|
case modeArchive:
|
|
err = runPipeForModeArchive(ctx, instance)
|
|
|
|
case modeBinary:
|
|
// Loop over all builds, because we want to publish every build to Artifactory
|
|
for _, build := range ctx.Config.Builds {
|
|
if err = runPipeForModeBinary(ctx, instance, build); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
default:
|
|
err = fmt.Errorf("artifactory: mode \"%s\" not supported", v)
|
|
log.WithFields(log.Fields{
|
|
"instance": instance.Name,
|
|
"mode": v,
|
|
}).Error(err.Error())
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runPipeForModeArchive uploads all ctx.Artifacts to instance
|
|
func runPipeForModeArchive(ctx *context.Context, instance config.Artifactory) error {
|
|
sem := make(chan bool, ctx.Parallelism)
|
|
var g errgroup.Group
|
|
|
|
// Get all artifacts and upload them
|
|
for _, artifact := range ctx.Artifacts {
|
|
sem <- true
|
|
artifact := artifact
|
|
g.Go(func() error {
|
|
defer func() {
|
|
<-sem
|
|
}()
|
|
|
|
return uploadArchive(ctx, instance, artifact)
|
|
})
|
|
}
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
// uploadArchive will upload artifact in mode archive
|
|
func uploadArchive(ctx *context.Context, instance config.Artifactory, artifact string) error {
|
|
var path = filepath.Join(ctx.Config.Dist, artifact)
|
|
return uploadAssetAndLog(ctx, instance, path, nil)
|
|
}
|
|
|
|
// uploadBinary will upload the current build and the current target in mode binary
|
|
func uploadBinary(ctx *context.Context, instance config.Artifactory, build config.Build, target buildtarget.Target) error {
|
|
binary, err := getBinaryForUploadPerBuild(ctx, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return uploadAssetAndLog(ctx, instance, binary.Path, &target)
|
|
}
|
|
|
|
// uploadAssetAndLog uploads file to target and logs all actions
|
|
func uploadAssetAndLog(ctx *context.Context, instance config.Artifactory, path string, target *buildtarget.Target) error {
|
|
secret := os.Getenv(fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)))
|
|
|
|
// Generate the target url
|
|
targetURL, err := resolveTargetTemplate(ctx, instance, target)
|
|
if err != nil {
|
|
msg := "artifactory: 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(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close() // nolint: errcheck
|
|
_, name := filepath.Split(path)
|
|
|
|
// The target url needs to contain the artifact name
|
|
if !strings.HasSuffix(targetURL, "/") {
|
|
targetURL += "/"
|
|
}
|
|
targetURL += name
|
|
|
|
artifact, _, err := uploadAssetToArtifactory(ctx, targetURL, instance.Username, secret, file)
|
|
if err != nil {
|
|
msg := "artifactory: 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": artifact.DownloadURI,
|
|
}).Info("uploaded successful")
|
|
|
|
return nil
|
|
}
|
|
|
|
// runPipeForModeBinary uploads all configured builds to instance
|
|
func runPipeForModeBinary(ctx *context.Context, instance config.Artifactory, build config.Build) error {
|
|
sem := make(chan bool, ctx.Parallelism)
|
|
var g errgroup.Group
|
|
|
|
// Lets generate the build matrix, because we want
|
|
// to publish every target to Artifactory
|
|
for _, target := range buildtarget.All(build) {
|
|
sem <- true
|
|
target := target
|
|
build := build
|
|
g.Go(func() error {
|
|
defer func() {
|
|
<-sem
|
|
}()
|
|
|
|
return uploadBinary(ctx, instance, build, target)
|
|
})
|
|
}
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
// getBinaryForUploadPerBuild determines the correct binary for the upload
|
|
func getBinaryForUploadPerBuild(ctx *context.Context, target buildtarget.Target) (*context.Binary, error) {
|
|
var group = ctx.Binaries[target.String()]
|
|
if group == nil {
|
|
return nil, fmt.Errorf("binary for build target %s not found", target.String())
|
|
}
|
|
|
|
var binary context.Binary
|
|
for _, binaries := range group {
|
|
for _, b := range binaries {
|
|
binary = b
|
|
break
|
|
}
|
|
break
|
|
}
|
|
|
|
return &binary, nil
|
|
}
|
|
|
|
// 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.Artifactory, target *buildtarget.Target) (string, error) {
|
|
data := targetData{
|
|
Version: ctx.Version,
|
|
Tag: ctx.Git.CurrentTag,
|
|
ProjectName: ctx.Config.ProjectName,
|
|
}
|
|
|
|
// Only supported in mode binary
|
|
if target != nil {
|
|
data.Os = replace(ctx.Config.Archive.Replacements, target.OS)
|
|
data.Arch = replace(ctx.Config.Archive.Replacements, target.Arch)
|
|
data.Arm = replace(ctx.Config.Archive.Replacements, target.Arm)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// uploadAssetToArtifactory uploads the asset file to target
|
|
func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *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(artifactoryResponse)
|
|
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 interface{}) (*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
|
|
}
|
|
|
|
err = json.NewDecoder(resp.Body).Decode(v)
|
|
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
|
|
Errors []Error `json:"errors"` // more detail on individual errors
|
|
}
|
|
|
|
func (r *errorResponse) Error() string {
|
|
return fmt.Sprintf("%v %v: %d %+v",
|
|
r.Response.Request.Method, r.Response.Request.URL,
|
|
r.Response.StatusCode, r.Errors)
|
|
}
|
|
|
|
// An Error reports more details on an individual error in an ErrorResponse.
|
|
type Error struct {
|
|
Status int `json:"status"` // Error code
|
|
Message string `json:"message"` // Message describing the error.
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
return fmt.Sprintf("%v (%v)", e.Message, e.Status)
|
|
}
|
|
|
|
// checkResponse checks the API 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 either no response
|
|
// body, or a JSON response body that maps to ErrorResponse. Any other
|
|
// response body will be silently ignored.
|
|
func checkResponse(r *http.Response) error {
|
|
if c := r.StatusCode; 200 <= c && c <= 299 {
|
|
return nil
|
|
}
|
|
errorResponse := &errorResponse{Response: r}
|
|
data, err := ioutil.ReadAll(r.Body)
|
|
if err == nil && data != nil {
|
|
err := json.Unmarshal(data, errorResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return errorResponse
|
|
}
|