github.com/tevino/goreleaser@v0.92.0/internal/http/http.go (about)

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