github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/builder/remotecontext/remote.go (about)

     1  package remotecontext
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"regexp"
    10  
    11  	"github.com/docker/docker/builder"
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  // When downloading remote contexts, limit the amount (in bytes)
    16  // to be read from the response body in order to detect its Content-Type
    17  const maxPreambleLength = 100
    18  
    19  const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
    20  
    21  var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
    22  
    23  // MakeRemoteContext downloads a context from remoteURL and returns it.
    24  //
    25  // If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of
    26  // maxPreambleLength bytes from the body to help detecting the MIME type.
    27  // Look at acceptableRemoteMIME for more details.
    28  //
    29  // If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected
    30  // to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not).
    31  // In either case, an (assumed) tar stream is passed to MakeTarSumContext whose result is returned.
    32  func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (builder.Source, error) {
    33  	f, err := GetWithStatusError(remoteURL)
    34  	if err != nil {
    35  		return nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err)
    36  	}
    37  	defer f.Body.Close()
    38  
    39  	var contextReader io.ReadCloser
    40  	if contentTypeHandlers != nil {
    41  		contentType := f.Header.Get("Content-Type")
    42  		clen := f.ContentLength
    43  
    44  		contentType, contextReader, err = inspectResponse(contentType, f.Body, clen)
    45  		if err != nil {
    46  			return nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err)
    47  		}
    48  		defer contextReader.Close()
    49  
    50  		// This loop tries to find a content-type handler for the detected content-type.
    51  		// If it could not find one from the caller-supplied map, it tries the empty content-type `""`
    52  		// which is interpreted as a fallback handler (usually used for raw tar contexts).
    53  		for _, ct := range []string{contentType, ""} {
    54  			if fn, ok := contentTypeHandlers[ct]; ok {
    55  				defer contextReader.Close()
    56  				if contextReader, err = fn(contextReader); err != nil {
    57  					return nil, err
    58  				}
    59  				break
    60  			}
    61  		}
    62  	}
    63  
    64  	// Pass through - this is a pre-packaged context, presumably
    65  	// with a Dockerfile with the right name inside it.
    66  	return MakeTarSumContext(contextReader)
    67  }
    68  
    69  // GetWithStatusError does an http.Get() and returns an error if the
    70  // status code is 4xx or 5xx.
    71  func GetWithStatusError(url string) (resp *http.Response, err error) {
    72  	if resp, err = http.Get(url); err != nil {
    73  		return nil, err
    74  	}
    75  	if resp.StatusCode < 400 {
    76  		return resp, nil
    77  	}
    78  	msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status)
    79  	body, err := ioutil.ReadAll(resp.Body)
    80  	resp.Body.Close()
    81  	if err != nil {
    82  		return nil, errors.Wrapf(err, msg+": error reading body")
    83  	}
    84  	return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body))
    85  }
    86  
    87  // inspectResponse looks into the http response data at r to determine whether its
    88  // content-type is on the list of acceptable content types for remote build contexts.
    89  // This function returns:
    90  //    - a string representation of the detected content-type
    91  //    - an io.Reader for the response body
    92  //    - an error value which will be non-nil either when something goes wrong while
    93  //      reading bytes from r or when the detected content-type is not acceptable.
    94  func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) {
    95  	plen := clen
    96  	if plen <= 0 || plen > maxPreambleLength {
    97  		plen = maxPreambleLength
    98  	}
    99  
   100  	preamble := make([]byte, plen, plen)
   101  	rlen, err := r.Read(preamble)
   102  	if rlen == 0 {
   103  		return ct, r, errors.New("empty response")
   104  	}
   105  	if err != nil && err != io.EOF {
   106  		return ct, r, err
   107  	}
   108  
   109  	preambleR := bytes.NewReader(preamble[:rlen])
   110  	bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
   111  	// Some web servers will use application/octet-stream as the default
   112  	// content type for files without an extension (e.g. 'Dockerfile')
   113  	// so if we receive this value we better check for text content
   114  	contentType := ct
   115  	if len(ct) == 0 || ct == mimeTypes.OctetStream {
   116  		contentType, _, err = detectContentType(preamble)
   117  		if err != nil {
   118  			return contentType, bodyReader, err
   119  		}
   120  	}
   121  
   122  	contentType = selectAcceptableMIME(contentType)
   123  	var cterr error
   124  	if len(contentType) == 0 {
   125  		cterr = fmt.Errorf("unsupported Content-Type %q", ct)
   126  		contentType = ct
   127  	}
   128  
   129  	return contentType, bodyReader, cterr
   130  }
   131  
   132  func selectAcceptableMIME(ct string) string {
   133  	return mimeRe.FindString(ct)
   134  }