2015-09-06 17:26:40 +00:00
|
|
|
package builder
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"regexp"
|
|
|
|
|
2016-01-20 23:32:02 +00:00
|
|
|
"github.com/docker/docker/pkg/archive"
|
2015-09-06 17:26:40 +00:00
|
|
|
"github.com/docker/docker/pkg/httputils"
|
2016-01-20 23:32:02 +00:00
|
|
|
"github.com/docker/docker/pkg/urlutil"
|
2015-09-06 17:26:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// When downloading remote contexts, limit the amount (in bytes)
|
|
|
|
// to be read from the response body in order to detect its Content-Type
|
|
|
|
const maxPreambleLength = 100
|
|
|
|
|
|
|
|
const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
|
|
|
|
|
|
|
|
var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
|
|
|
|
|
|
|
|
// MakeRemoteContext downloads a context from remoteURL and returns it.
|
|
|
|
//
|
|
|
|
// If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of
|
|
|
|
// maxPreambleLength bytes from the body to help detecting the MIME type.
|
|
|
|
// Look at acceptableRemoteMIME for more details.
|
|
|
|
//
|
|
|
|
// If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected
|
|
|
|
// to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not).
|
|
|
|
// In either case, an (assumed) tar stream is passed to MakeTarSumContext whose result is returned.
|
|
|
|
func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (ModifiableContext, error) {
|
|
|
|
f, err := httputils.Download(remoteURL)
|
|
|
|
if err != nil {
|
2016-10-17 07:19:10 +00:00
|
|
|
return nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err)
|
2015-09-06 17:26:40 +00:00
|
|
|
}
|
|
|
|
defer f.Body.Close()
|
|
|
|
|
|
|
|
var contextReader io.ReadCloser
|
|
|
|
if contentTypeHandlers != nil {
|
|
|
|
contentType := f.Header.Get("Content-Type")
|
|
|
|
clen := f.ContentLength
|
|
|
|
|
|
|
|
contentType, contextReader, err = inspectResponse(contentType, f.Body, clen)
|
|
|
|
if err != nil {
|
2016-10-17 07:19:10 +00:00
|
|
|
return nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err)
|
2015-09-06 17:26:40 +00:00
|
|
|
}
|
|
|
|
defer contextReader.Close()
|
|
|
|
|
|
|
|
// This loop tries to find a content-type handler for the detected content-type.
|
|
|
|
// If it could not find one from the caller-supplied map, it tries the empty content-type `""`
|
|
|
|
// which is interpreted as a fallback handler (usually used for raw tar contexts).
|
|
|
|
for _, ct := range []string{contentType, ""} {
|
|
|
|
if fn, ok := contentTypeHandlers[ct]; ok {
|
|
|
|
defer contextReader.Close()
|
|
|
|
if contextReader, err = fn(contextReader); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass through - this is a pre-packaged context, presumably
|
|
|
|
// with a Dockerfile with the right name inside it.
|
|
|
|
return MakeTarSumContext(contextReader)
|
|
|
|
}
|
|
|
|
|
2016-01-20 23:32:02 +00:00
|
|
|
// DetectContextFromRemoteURL returns a context and in certain cases the name of the dockerfile to be used
|
|
|
|
// irrespective of user input.
|
|
|
|
// progressReader is only used if remoteURL is actually a URL (not empty, and not a Git endpoint).
|
|
|
|
func DetectContextFromRemoteURL(r io.ReadCloser, remoteURL string, createProgressReader func(in io.ReadCloser) io.ReadCloser) (context ModifiableContext, dockerfileName string, err error) {
|
|
|
|
switch {
|
|
|
|
case remoteURL == "":
|
|
|
|
context, err = MakeTarSumContext(r)
|
|
|
|
case urlutil.IsGitURL(remoteURL):
|
|
|
|
context, err = MakeGitContext(remoteURL)
|
|
|
|
case urlutil.IsURL(remoteURL):
|
|
|
|
context, err = MakeRemoteContext(remoteURL, map[string]func(io.ReadCloser) (io.ReadCloser, error){
|
|
|
|
httputils.MimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
|
|
|
|
dockerfile, err := ioutil.ReadAll(rc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// dockerfileName is set to signal that the remote was interpreted as a single Dockerfile, in which case the caller
|
|
|
|
// should use dockerfileName as the new name for the Dockerfile, irrespective of any other user input.
|
2016-02-11 19:59:59 +00:00
|
|
|
dockerfileName = DefaultDockerfileName
|
2016-01-20 23:32:02 +00:00
|
|
|
|
|
|
|
// TODO: return a context without tarsum
|
2016-10-20 23:40:59 +00:00
|
|
|
r, err := archive.Generate(dockerfileName, string(dockerfile))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ioutil.NopCloser(r), nil
|
2016-01-20 23:32:02 +00:00
|
|
|
},
|
|
|
|
// fallback handler (tar context)
|
|
|
|
"": func(rc io.ReadCloser) (io.ReadCloser, error) {
|
|
|
|
return createProgressReader(rc), nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
default:
|
|
|
|
err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-09-06 17:26:40 +00:00
|
|
|
// inspectResponse looks into the http response data at r to determine whether its
|
|
|
|
// content-type is on the list of acceptable content types for remote build contexts.
|
|
|
|
// This function returns:
|
|
|
|
// - a string representation of the detected content-type
|
|
|
|
// - an io.Reader for the response body
|
|
|
|
// - an error value which will be non-nil either when something goes wrong while
|
|
|
|
// reading bytes from r or when the detected content-type is not acceptable.
|
|
|
|
func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) {
|
|
|
|
plen := clen
|
|
|
|
if plen <= 0 || plen > maxPreambleLength {
|
|
|
|
plen = maxPreambleLength
|
|
|
|
}
|
|
|
|
|
|
|
|
preamble := make([]byte, plen, plen)
|
|
|
|
rlen, err := r.Read(preamble)
|
|
|
|
if rlen == 0 {
|
2016-10-17 07:19:10 +00:00
|
|
|
return ct, r, errors.New("empty response")
|
2015-09-06 17:26:40 +00:00
|
|
|
}
|
|
|
|
if err != nil && err != io.EOF {
|
|
|
|
return ct, r, err
|
|
|
|
}
|
|
|
|
|
|
|
|
preambleR := bytes.NewReader(preamble)
|
|
|
|
bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
|
|
|
|
// Some web servers will use application/octet-stream as the default
|
|
|
|
// content type for files without an extension (e.g. 'Dockerfile')
|
|
|
|
// so if we receive this value we better check for text content
|
|
|
|
contentType := ct
|
|
|
|
if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
|
|
|
|
contentType, _, err = httputils.DetectContentType(preamble)
|
|
|
|
if err != nil {
|
|
|
|
return contentType, bodyReader, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
contentType = selectAcceptableMIME(contentType)
|
|
|
|
var cterr error
|
|
|
|
if len(contentType) == 0 {
|
|
|
|
cterr = fmt.Errorf("unsupported Content-Type %q", ct)
|
|
|
|
contentType = ct
|
|
|
|
}
|
|
|
|
|
|
|
|
return contentType, bodyReader, cterr
|
|
|
|
}
|
|
|
|
|
|
|
|
func selectAcceptableMIME(ct string) string {
|
|
|
|
return mimeRe.FindString(ct)
|
|
|
|
}
|