github.com/skf/moby@v1.13.1/builder/remote.go (about)

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