github.com/szyn/goreleaser@v0.76.1-0.20180517112710-333da09a1297/pipeline/artifactory/artifactory.go (about)

     1  // Package artifactory provides a Pipe that push to artifactory
     2  package artifactory
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"html/template"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"strings"
    15  
    16  	"github.com/apex/log"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/sync/errgroup"
    19  
    20  	"github.com/goreleaser/goreleaser/config"
    21  	"github.com/goreleaser/goreleaser/context"
    22  	"github.com/goreleaser/goreleaser/internal/artifact"
    23  	"github.com/goreleaser/goreleaser/pipeline"
    24  )
    25  
    26  // artifactoryResponse reflects the response after an upload request
    27  // to Artifactory.
    28  type artifactoryResponse struct {
    29  	Repo              string               `json:"repo,omitempty"`
    30  	Path              string               `json:"path,omitempty"`
    31  	Created           string               `json:"created,omitempty"`
    32  	CreatedBy         string               `json:"createdBy,omitempty"`
    33  	DownloadURI       string               `json:"downloadUri,omitempty"`
    34  	MimeType          string               `json:"mimeType,omitempty"`
    35  	Size              string               `json:"size,omitempty"`
    36  	Checksums         artifactoryChecksums `json:"checksums,omitempty"`
    37  	OriginalChecksums artifactoryChecksums `json:"originalChecksums,omitempty"`
    38  	URI               string               `json:"uri,omitempty"`
    39  }
    40  
    41  // artifactoryChecksums reflects the checksums generated by
    42  // Artifactory
    43  type artifactoryChecksums struct {
    44  	SHA1   string `json:"sha1,omitempty"`
    45  	MD5    string `json:"md5,omitempty"`
    46  	SHA256 string `json:"sha256,omitempty"`
    47  }
    48  
    49  const (
    50  	modeBinary  = "binary"
    51  	modeArchive = "archive"
    52  )
    53  
    54  // Pipe for Artifactory
    55  type Pipe struct{}
    56  
    57  // String returns the description of the pipe
    58  func (Pipe) String() string {
    59  	return "releasing to Artifactory"
    60  }
    61  
    62  // Default sets the pipe defaults
    63  func (Pipe) Default(ctx *context.Context) error {
    64  	if len(ctx.Config.Artifactories) == 0 {
    65  		return nil
    66  	}
    67  
    68  	// Check if a mode was set
    69  	for i := range ctx.Config.Artifactories {
    70  		if ctx.Config.Artifactories[i].Mode == "" {
    71  			ctx.Config.Artifactories[i].Mode = modeArchive
    72  		}
    73  	}
    74  
    75  	return nil
    76  }
    77  
    78  // Run the pipe
    79  //
    80  // Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact
    81  func (Pipe) Run(ctx *context.Context) error {
    82  	if len(ctx.Config.Artifactories) == 0 {
    83  		return pipeline.Skip("artifactory section is not configured")
    84  	}
    85  
    86  	// Check requirements for every instance we have configured.
    87  	// If not fulfilled, we can skip this pipeline
    88  	for _, instance := range ctx.Config.Artifactories {
    89  		if instance.Target == "" {
    90  			return pipeline.Skip("artifactory section is not configured properly (missing target)")
    91  		}
    92  
    93  		if instance.Username == "" {
    94  			return pipeline.Skip("artifactory section is not configured properly (missing username)")
    95  		}
    96  
    97  		if instance.Name == "" {
    98  			return pipeline.Skip("artifactory section is not configured properly (missing name)")
    99  		}
   100  
   101  		envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name))
   102  		if _, ok := ctx.Env[envName]; !ok {
   103  			return pipeline.Skip(fmt.Sprintf("missing secret for artifactory instance %s", instance.Name))
   104  		}
   105  	}
   106  
   107  	return doRun(ctx)
   108  }
   109  
   110  func doRun(ctx *context.Context) error {
   111  	if ctx.SkipPublish {
   112  		return pipeline.ErrSkipPublishEnabled
   113  	}
   114  
   115  	// Handle every configured artifactory instance
   116  	for _, instance := range ctx.Config.Artifactories {
   117  		// We support two different modes
   118  		//	- "archive": Upload all artifacts
   119  		//	- "binary": Upload only the raw binaries
   120  		var filter artifact.Filter
   121  		switch v := strings.ToLower(instance.Mode); v {
   122  		case modeArchive:
   123  			filter = artifact.Or(
   124  				artifact.ByType(artifact.UploadableArchive),
   125  				artifact.ByType(artifact.LinuxPackage),
   126  			)
   127  		case modeBinary:
   128  			filter = artifact.ByType(artifact.UploadableBinary)
   129  		default:
   130  			err := fmt.Errorf("artifactory: mode \"%s\" not supported", v)
   131  			log.WithFields(log.Fields{
   132  				"instance": instance.Name,
   133  				"mode":     v,
   134  			}).Error(err.Error())
   135  			return err
   136  		}
   137  
   138  		if err := runPipeByFilter(ctx, instance, filter); err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func runPipeByFilter(ctx *context.Context, instance config.Artifactory, filter artifact.Filter) error {
   147  	sem := make(chan bool, ctx.Parallelism)
   148  	var g errgroup.Group
   149  	for _, artifact := range ctx.Artifacts.Filter(filter).List() {
   150  		sem <- true
   151  		artifact := artifact
   152  		g.Go(func() error {
   153  			defer func() {
   154  				<-sem
   155  			}()
   156  			return uploadAsset(ctx, instance, artifact)
   157  		})
   158  	}
   159  	return g.Wait()
   160  }
   161  
   162  // uploadAsset uploads file to target and logs all actions
   163  func uploadAsset(ctx *context.Context, instance config.Artifactory, artifact artifact.Artifact) error {
   164  	envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name))
   165  	secret := ctx.Env[envName]
   166  
   167  	// Generate the target url
   168  	targetURL, err := resolveTargetTemplate(ctx, instance, artifact)
   169  	if err != nil {
   170  		msg := "artifactory: error while building the target url"
   171  		log.WithField("instance", instance.Name).WithError(err).Error(msg)
   172  		return errors.Wrap(err, msg)
   173  	}
   174  
   175  	// Handle the artifact
   176  	file, err := os.Open(artifact.Path)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	defer file.Close() // nolint: errcheck
   181  
   182  	// The target url needs to contain the artifact name
   183  	if !strings.HasSuffix(targetURL, "/") {
   184  		targetURL += "/"
   185  	}
   186  	targetURL += artifact.Name
   187  
   188  	uploaded, _, err := uploadAssetToArtifactory(ctx, targetURL, instance.Username, secret, file)
   189  	if err != nil {
   190  		msg := "artifactory: upload failed"
   191  		log.WithError(err).WithFields(log.Fields{
   192  			"instance": instance.Name,
   193  			"username": instance.Username,
   194  		}).Error(msg)
   195  		return errors.Wrap(err, msg)
   196  	}
   197  
   198  	log.WithFields(log.Fields{
   199  		"instance": instance.Name,
   200  		"mode":     instance.Mode,
   201  		"uri":      uploaded.DownloadURI,
   202  	}).Info("uploaded successful")
   203  
   204  	return nil
   205  }
   206  
   207  // targetData is used as a template struct for
   208  // Artifactory.Target
   209  type targetData struct {
   210  	Version     string
   211  	Tag         string
   212  	ProjectName string
   213  
   214  	// Only supported in mode binary
   215  	Os   string
   216  	Arch string
   217  	Arm  string
   218  }
   219  
   220  // resolveTargetTemplate returns the resolved target template with replaced variables
   221  // Those variables can be replaced by the given context, goos, goarch, goarm and more
   222  func resolveTargetTemplate(ctx *context.Context, artifactory config.Artifactory, artifact artifact.Artifact) (string, error) {
   223  	data := targetData{
   224  		Version:     ctx.Version,
   225  		Tag:         ctx.Git.CurrentTag,
   226  		ProjectName: ctx.Config.ProjectName,
   227  	}
   228  
   229  	if artifactory.Mode == modeBinary {
   230  		data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos)
   231  		data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch)
   232  		data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm)
   233  	}
   234  
   235  	var out bytes.Buffer
   236  	t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target)
   237  	if err != nil {
   238  		return "", err
   239  	}
   240  	err = t.Execute(&out, data)
   241  	return out.String(), err
   242  }
   243  
   244  func replace(replacements map[string]string, original string) string {
   245  	result := replacements[original]
   246  	if result == "" {
   247  		return original
   248  	}
   249  	return result
   250  }
   251  
   252  // uploadAssetToArtifactory uploads the asset file to target
   253  func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) {
   254  	stat, err := file.Stat()
   255  	if err != nil {
   256  		return nil, nil, err
   257  	}
   258  	if stat.IsDir() {
   259  		return nil, nil, errors.New("the asset to upload can't be a directory")
   260  	}
   261  
   262  	req, err := newUploadRequest(target, username, secret, file, stat.Size())
   263  	if err != nil {
   264  		return nil, nil, err
   265  	}
   266  
   267  	asset := new(artifactoryResponse)
   268  	resp, err := executeHTTPRequest(ctx, req, asset)
   269  	if err != nil {
   270  		return nil, resp, err
   271  	}
   272  	return asset, resp, nil
   273  }
   274  
   275  // newUploadRequest creates a new http.Request for uploading
   276  func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) {
   277  	u, err := url.Parse(target)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	req, err := http.NewRequest("PUT", u.String(), reader)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	req.ContentLength = size
   287  	req.SetBasicAuth(username, secret)
   288  
   289  	return req, err
   290  }
   291  
   292  // executeHTTPRequest processes the http call with respect of context ctx
   293  func executeHTTPRequest(ctx *context.Context, req *http.Request, v interface{}) (*http.Response, error) {
   294  	resp, err := http.DefaultClient.Do(req)
   295  	if err != nil {
   296  		// If we got an error, and the context has been canceled,
   297  		// the context's error is probably more useful.
   298  		select {
   299  		case <-ctx.Done():
   300  			return nil, ctx.Err()
   301  		default:
   302  		}
   303  
   304  		return nil, err
   305  	}
   306  
   307  	defer resp.Body.Close() // nolint: errcheck
   308  
   309  	err = checkResponse(resp)
   310  	if err != nil {
   311  		// even though there was an error, we still return the response
   312  		// in case the caller wants to inspect it further
   313  		return resp, err
   314  	}
   315  
   316  	err = json.NewDecoder(resp.Body).Decode(v)
   317  	return resp, err
   318  }
   319  
   320  // An ErrorResponse reports one or more errors caused by an API request.
   321  type errorResponse struct {
   322  	Response *http.Response // HTTP response that caused this error
   323  	Errors   []Error        `json:"errors"` // more detail on individual errors
   324  }
   325  
   326  func (r *errorResponse) Error() string {
   327  	return fmt.Sprintf("%v %v: %d %+v",
   328  		r.Response.Request.Method, r.Response.Request.URL,
   329  		r.Response.StatusCode, r.Errors)
   330  }
   331  
   332  // An Error reports more details on an individual error in an ErrorResponse.
   333  type Error struct {
   334  	Status  int    `json:"status"`  // Error code
   335  	Message string `json:"message"` // Message describing the error.
   336  }
   337  
   338  // checkResponse checks the API response for errors, and returns them if
   339  // present. A response is considered an error if it has a status code outside
   340  // the 200 range.
   341  // API error responses are expected to have either no response
   342  // body, or a JSON response body that maps to ErrorResponse. Any other
   343  // response body will be silently ignored.
   344  func checkResponse(r *http.Response) error {
   345  	if c := r.StatusCode; 200 <= c && c <= 299 {
   346  		return nil
   347  	}
   348  	errorResponse := &errorResponse{Response: r}
   349  	data, err := ioutil.ReadAll(r.Body)
   350  	if err == nil && data != nil {
   351  		err := json.Unmarshal(data, errorResponse)
   352  		if err != nil {
   353  			return err
   354  		}
   355  	}
   356  	return errorResponse
   357  }