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 }