remote.go 5.6 KB

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