gitee.com/mirrors_opencollective/goreleaser@v0.45.0/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.Publish {
   112  		return pipeline.ErrSkipPublish
   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 err error
   121  		switch v := strings.ToLower(instance.Mode); v {
   122  		case modeArchive:
   123  			err = runPipeByFilter(ctx, instance, artifact.ByType(artifact.UploadableArchive))
   124  
   125  		case modeBinary:
   126  			err = runPipeByFilter(ctx, instance, artifact.ByType(artifact.UploadableBinary))
   127  
   128  		default:
   129  			err = fmt.Errorf("artifactory: mode \"%s\" not supported", v)
   130  			log.WithFields(log.Fields{
   131  				"instance": instance.Name,
   132  				"mode":     v,
   133  			}).Error(err.Error())
   134  		}
   135  
   136  		if err != nil {
   137  			return err
   138  		}
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func runPipeByFilter(ctx *context.Context, instance config.Artifactory, filter artifact.Filter) error {
   145  	sem := make(chan bool, ctx.Parallelism)
   146  	var g errgroup.Group
   147  	for _, artifact := range ctx.Artifacts.Filter(filter).List() {
   148  		sem <- true
   149  		artifact := artifact
   150  		g.Go(func() error {
   151  			defer func() {
   152  				<-sem
   153  			}()
   154  			return uploadAsset(ctx, instance, artifact)
   155  		})
   156  	}
   157  	return g.Wait()
   158  }
   159  
   160  // uploadAsset uploads file to target and logs all actions
   161  func uploadAsset(ctx *context.Context, instance config.Artifactory, artifact artifact.Artifact) error {
   162  	envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name))
   163  	secret := ctx.Env[envName]
   164  
   165  	// Generate the target url
   166  	targetURL, err := resolveTargetTemplate(ctx, instance, artifact)
   167  	if err != nil {
   168  		msg := "artifactory: error while building the target url"
   169  		log.WithField("instance", instance.Name).WithError(err).Error(msg)
   170  		return errors.Wrap(err, msg)
   171  	}
   172  
   173  	// Handle the artifact
   174  	file, err := os.Open(artifact.Path)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	defer file.Close() // nolint: errcheck
   179  
   180  	// The target url needs to contain the artifact name
   181  	if !strings.HasSuffix(targetURL, "/") {
   182  		targetURL += "/"
   183  	}
   184  	targetURL += artifact.Name
   185  
   186  	uploaded, _, err := uploadAssetToArtifactory(ctx, targetURL, instance.Username, secret, file)
   187  	if err != nil {
   188  		msg := "artifactory: upload failed"
   189  		log.WithError(err).WithFields(log.Fields{
   190  			"instance": instance.Name,
   191  			"username": instance.Username,
   192  		}).Error(msg)
   193  		return errors.Wrap(err, msg)
   194  	}
   195  
   196  	log.WithFields(log.Fields{
   197  		"instance": instance.Name,
   198  		"mode":     instance.Mode,
   199  		"uri":      uploaded.DownloadURI,
   200  	}).Info("uploaded successful")
   201  
   202  	return nil
   203  }
   204  
   205  // targetData is used as a template struct for
   206  // Artifactory.Target
   207  type targetData struct {
   208  	Version     string
   209  	Tag         string
   210  	ProjectName string
   211  
   212  	// Only supported in mode binary
   213  	Os   string
   214  	Arch string
   215  	Arm  string
   216  }
   217  
   218  // resolveTargetTemplate returns the resolved target template with replaced variables
   219  // Those variables can be replaced by the given context, goos, goarch, goarm and more
   220  func resolveTargetTemplate(ctx *context.Context, artifactory config.Artifactory, artifact artifact.Artifact) (string, error) {
   221  	data := targetData{
   222  		Version:     ctx.Version,
   223  		Tag:         ctx.Git.CurrentTag,
   224  		ProjectName: ctx.Config.ProjectName,
   225  	}
   226  
   227  	if artifactory.Mode == modeBinary {
   228  		data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos)
   229  		data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch)
   230  		data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm)
   231  	}
   232  
   233  	var out bytes.Buffer
   234  	t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target)
   235  	if err != nil {
   236  		return "", err
   237  	}
   238  	err = t.Execute(&out, data)
   239  	return out.String(), err
   240  }
   241  
   242  func replace(replacements map[string]string, original string) string {
   243  	result := replacements[original]
   244  	if result == "" {
   245  		return original
   246  	}
   247  	return result
   248  }
   249  
   250  // uploadAssetToArtifactory uploads the asset file to target
   251  func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) {
   252  	stat, err := file.Stat()
   253  	if err != nil {
   254  		return nil, nil, err
   255  	}
   256  	if stat.IsDir() {
   257  		return nil, nil, errors.New("the asset to upload can't be a directory")
   258  	}
   259  
   260  	req, err := newUploadRequest(target, username, secret, file, stat.Size())
   261  	if err != nil {
   262  		return nil, nil, err
   263  	}
   264  
   265  	asset := new(artifactoryResponse)
   266  	resp, err := executeHTTPRequest(ctx, req, asset)
   267  	if err != nil {
   268  		return nil, resp, err
   269  	}
   270  	return asset, resp, nil
   271  }
   272  
   273  // newUploadRequest creates a new http.Request for uploading
   274  func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) {
   275  	u, err := url.Parse(target)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	req, err := http.NewRequest("PUT", u.String(), reader)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  
   284  	req.ContentLength = size
   285  	req.SetBasicAuth(username, secret)
   286  
   287  	return req, err
   288  }
   289  
   290  // executeHTTPRequest processes the http call with respect of context ctx
   291  func executeHTTPRequest(ctx *context.Context, req *http.Request, v interface{}) (*http.Response, error) {
   292  	resp, err := http.DefaultClient.Do(req)
   293  	if err != nil {
   294  		// If we got an error, and the context has been canceled,
   295  		// the context's error is probably more useful.
   296  		select {
   297  		case <-ctx.Done():
   298  			return nil, ctx.Err()
   299  		default:
   300  		}
   301  
   302  		return nil, err
   303  	}
   304  
   305  	defer resp.Body.Close() // nolint: errcheck
   306  
   307  	err = checkResponse(resp)
   308  	if err != nil {
   309  		// even though there was an error, we still return the response
   310  		// in case the caller wants to inspect it further
   311  		return resp, err
   312  	}
   313  
   314  	err = json.NewDecoder(resp.Body).Decode(v)
   315  	return resp, err
   316  }
   317  
   318  // An ErrorResponse reports one or more errors caused by an API request.
   319  type errorResponse struct {
   320  	Response *http.Response // HTTP response that caused this error
   321  	Errors   []Error        `json:"errors"` // more detail on individual errors
   322  }
   323  
   324  func (r *errorResponse) Error() string {
   325  	return fmt.Sprintf("%v %v: %d %+v",
   326  		r.Response.Request.Method, r.Response.Request.URL,
   327  		r.Response.StatusCode, r.Errors)
   328  }
   329  
   330  // An Error reports more details on an individual error in an ErrorResponse.
   331  type Error struct {
   332  	Status  int    `json:"status"`  // Error code
   333  	Message string `json:"message"` // Message describing the error.
   334  }
   335  
   336  // checkResponse checks the API response for errors, and returns them if
   337  // present. A response is considered an error if it has a status code outside
   338  // the 200 range.
   339  // API error responses are expected to have either no response
   340  // body, or a JSON response body that maps to ErrorResponse. Any other
   341  // response body will be silently ignored.
   342  func checkResponse(r *http.Response) error {
   343  	if c := r.StatusCode; 200 <= c && c <= 299 {
   344  		return nil
   345  	}
   346  	errorResponse := &errorResponse{Response: r}
   347  	data, err := ioutil.ReadAll(r.Body)
   348  	if err == nil && data != nil {
   349  		err := json.Unmarshal(data, errorResponse)
   350  		if err != nil {
   351  			return err
   352  		}
   353  	}
   354  	return errorResponse
   355  }