github.com/tahsinrahman/goreleaser@v0.79.1/internal/http/http.go (about)

     1  // Package http implements functionality common to HTTP uploading pipelines.
     2  package http
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"html/template"
     8  	"io"
     9  	h "net/http"
    10  	"net/url"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/pkg/errors"
    16  	"golang.org/x/sync/errgroup"
    17  
    18  	"github.com/goreleaser/goreleaser/config"
    19  	"github.com/goreleaser/goreleaser/context"
    20  	"github.com/goreleaser/goreleaser/internal/artifact"
    21  	"github.com/goreleaser/goreleaser/pipeline"
    22  )
    23  
    24  const (
    25  	// ModeBinary uploads only compiled binaries
    26  	ModeBinary = "binary"
    27  	// ModeArchive uploads release archives
    28  	ModeArchive = "archive"
    29  )
    30  
    31  type asset struct {
    32  	ReadCloser io.ReadCloser
    33  	Size       int64
    34  }
    35  
    36  type assetOpenFunc func(string, *artifact.Artifact) (*asset, error)
    37  
    38  var assetOpen assetOpenFunc
    39  
    40  func init() {
    41  	assetOpenReset()
    42  }
    43  
    44  func assetOpenReset() {
    45  	assetOpen = assetOpenDefault
    46  }
    47  
    48  func assetOpenDefault(kind string, a *artifact.Artifact) (*asset, error) {
    49  	f, err := os.Open(a.Path)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	s, err := f.Stat()
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	if s.IsDir() {
    58  		return nil, errors.Errorf("%s: upload failed: the asset to upload can't be a directory", kind)
    59  	}
    60  	return &asset{
    61  		ReadCloser: f,
    62  		Size:       s.Size(),
    63  	}, nil
    64  }
    65  
    66  // Defaults sets default configuration options on Put structs
    67  func Defaults(puts []config.Put) error {
    68  	for i := range puts {
    69  		defaults(&puts[i])
    70  	}
    71  	return nil
    72  }
    73  
    74  func defaults(put *config.Put) {
    75  	if put.Mode == "" {
    76  		put.Mode = ModeArchive
    77  	}
    78  }
    79  
    80  // CheckConfig validates a Put configuration returning a descriptive error when appropriate
    81  func CheckConfig(ctx *context.Context, put *config.Put, kind string) error {
    82  
    83  	if put.Target == "" {
    84  		return misconfigured(kind, put, "missing target")
    85  	}
    86  
    87  	if put.Username == "" {
    88  		return misconfigured(kind, put, "missing username")
    89  	}
    90  
    91  	if put.Name == "" {
    92  		return misconfigured(kind, put, "missing name")
    93  	}
    94  
    95  	if put.Mode != ModeArchive && put.Mode != ModeBinary {
    96  		return misconfigured(kind, put, "mode must be 'binary' or 'archive'")
    97  	}
    98  
    99  	envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name))
   100  	if _, ok := ctx.Env[envName]; !ok {
   101  		return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName))
   102  	}
   103  
   104  	return nil
   105  
   106  }
   107  
   108  func misconfigured(kind string, upload *config.Put, reason string) error {
   109  	return pipeline.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason))
   110  }
   111  
   112  // ResponseChecker is a function capable of validating an http server response.
   113  // It must return the location of the uploaded asset or the error when the
   114  // response must be considered a failure.
   115  type ResponseChecker func(*h.Response) (string, error)
   116  
   117  // Upload does the actual uploading work
   118  func Upload(ctx *context.Context, puts []config.Put, kind string, check ResponseChecker) error {
   119  	if ctx.SkipPublish {
   120  		return pipeline.ErrSkipPublishEnabled
   121  	}
   122  
   123  	// Handle every configured put
   124  	for _, put := range puts {
   125  		filters := []artifact.Filter{}
   126  		if put.Checksum {
   127  			filters = append(filters, artifact.ByType(artifact.Checksum))
   128  		}
   129  		if put.Signature {
   130  			filters = append(filters, artifact.ByType(artifact.Signature))
   131  		}
   132  		// We support two different modes
   133  		//	- "archive": Upload all artifacts
   134  		//	- "binary": Upload only the raw binaries
   135  		switch v := strings.ToLower(put.Mode); v {
   136  		case ModeArchive:
   137  			filters = append(filters,
   138  				artifact.ByType(artifact.UploadableArchive),
   139  				artifact.ByType(artifact.LinuxPackage))
   140  		case ModeBinary:
   141  			filters = append(filters,
   142  				artifact.ByType(artifact.UploadableBinary))
   143  		default:
   144  			err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v)
   145  			log.WithFields(log.Fields{
   146  				kind:   put.Name,
   147  				"mode": v,
   148  			}).Error(err.Error())
   149  			return err
   150  		}
   151  		if err := runPipeByFilter(ctx, put, artifact.Or(filters...), kind, check); err != nil {
   152  			return err
   153  		}
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func runPipeByFilter(ctx *context.Context, put config.Put, filter artifact.Filter, kind string, check ResponseChecker) error {
   160  	sem := make(chan bool, ctx.Parallelism)
   161  	var g errgroup.Group
   162  	for _, artifact := range ctx.Artifacts.Filter(filter).List() {
   163  		sem <- true
   164  		artifact := artifact
   165  		g.Go(func() error {
   166  			defer func() {
   167  				<-sem
   168  			}()
   169  			return uploadAsset(ctx, put, artifact, kind, check)
   170  		})
   171  	}
   172  	return g.Wait()
   173  }
   174  
   175  // uploadAsset uploads file to target and logs all actions
   176  func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error {
   177  	envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name))
   178  	secret := ctx.Env[envName]
   179  
   180  	// Generate the target url
   181  	targetURL, err := resolveTargetTemplate(ctx, put, artifact)
   182  	if err != nil {
   183  		msg := fmt.Sprintf("%s: error while building the target url", kind)
   184  		log.WithField("instance", put.Name).WithError(err).Error(msg)
   185  		return errors.Wrap(err, msg)
   186  	}
   187  
   188  	// Handle the artifact
   189  	asset, err := assetOpen(kind, &artifact)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	defer asset.ReadCloser.Close() // nolint: errcheck
   194  
   195  	// The target url needs to contain the artifact name
   196  	if !strings.HasSuffix(targetURL, "/") {
   197  		targetURL += "/"
   198  	}
   199  	targetURL += artifact.Name
   200  
   201  	location, _, err := uploadAssetToServer(ctx, targetURL, put.Username, secret, asset, check)
   202  	if err != nil {
   203  		msg := fmt.Sprintf("%s: upload failed", kind)
   204  		log.WithError(err).WithFields(log.Fields{
   205  			"instance": put.Name,
   206  			"username": put.Username,
   207  		}).Error(msg)
   208  		return errors.Wrap(err, msg)
   209  	}
   210  
   211  	log.WithFields(log.Fields{
   212  		"instance": put.Name,
   213  		"mode":     put.Mode,
   214  		"uri":      location,
   215  	}).Info("uploaded successful")
   216  
   217  	return nil
   218  }
   219  
   220  // uploadAssetToServer uploads the asset file to target
   221  func uploadAssetToServer(ctx *context.Context, target, username, secret string, a *asset, check ResponseChecker) (string, *h.Response, error) {
   222  	req, err := newUploadRequest(target, username, secret, a)
   223  	if err != nil {
   224  		return "", nil, err
   225  	}
   226  
   227  	loc, resp, err := executeHTTPRequest(ctx, req, check)
   228  	if err != nil {
   229  		return "", resp, err
   230  	}
   231  	return loc, resp, nil
   232  }
   233  
   234  // newUploadRequest creates a new h.Request for uploading
   235  func newUploadRequest(target, username, secret string, a *asset) (*h.Request, error) {
   236  	u, err := url.Parse(target)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	req, err := h.NewRequest("PUT", u.String(), a.ReadCloser)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	req.ContentLength = a.Size
   246  	req.SetBasicAuth(username, secret)
   247  
   248  	return req, err
   249  }
   250  
   251  // executeHTTPRequest processes the http call with respect of context ctx
   252  func executeHTTPRequest(ctx *context.Context, req *h.Request, check ResponseChecker) (string, *h.Response, error) {
   253  	resp, err := h.DefaultClient.Do(req)
   254  	if err != nil {
   255  		// If we got an error, and the context has been canceled,
   256  		// the context's error is probably more useful.
   257  		select {
   258  		case <-ctx.Done():
   259  			return "", nil, ctx.Err()
   260  		default:
   261  		}
   262  
   263  		return "", nil, err
   264  	}
   265  
   266  	defer resp.Body.Close() // nolint: errcheck
   267  
   268  	loc, err := check(resp)
   269  	if err != nil {
   270  		// even though there was an error, we still return the response
   271  		// in case the caller wants to inspect it further
   272  		return "", resp, err
   273  	}
   274  
   275  	return loc, resp, err
   276  }
   277  
   278  // targetData is used as a template struct for
   279  // Artifactory.Target
   280  type targetData struct {
   281  	Version     string
   282  	Tag         string
   283  	ProjectName string
   284  
   285  	// Only supported in mode binary
   286  	Os   string
   287  	Arch string
   288  	Arm  string
   289  }
   290  
   291  // resolveTargetTemplate returns the resolved target template with replaced variables
   292  // Those variables can be replaced by the given context, goos, goarch, goarm and more
   293  func resolveTargetTemplate(ctx *context.Context, artifactory config.Put, artifact artifact.Artifact) (string, error) {
   294  	data := targetData{
   295  		Version:     ctx.Version,
   296  		Tag:         ctx.Git.CurrentTag,
   297  		ProjectName: ctx.Config.ProjectName,
   298  	}
   299  
   300  	if artifactory.Mode == ModeBinary {
   301  		data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos)
   302  		data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch)
   303  		data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm)
   304  	}
   305  
   306  	var out bytes.Buffer
   307  	t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target)
   308  	if err != nil {
   309  		return "", err
   310  	}
   311  	err = t.Execute(&out, data)
   312  	return out.String(), err
   313  }
   314  
   315  func replace(replacements map[string]string, original string) string {
   316  	result := replacements[original]
   317  	if result == "" {
   318  		return original
   319  	}
   320  	return result
   321  }