detect.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package remotecontext
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "os"
  7. "strings"
  8. "github.com/containerd/continuity/driver"
  9. "github.com/docker/docker/api/types/backend"
  10. "github.com/docker/docker/builder"
  11. "github.com/docker/docker/builder/dockerfile/parser"
  12. "github.com/docker/docker/builder/dockerignore"
  13. "github.com/docker/docker/pkg/fileutils"
  14. "github.com/docker/docker/pkg/urlutil"
  15. "github.com/pkg/errors"
  16. "github.com/sirupsen/logrus"
  17. )
  18. // ClientSessionRemote is identifier for client-session context transport
  19. const ClientSessionRemote = "client-session"
  20. // Detect returns a context and dockerfile from remote location or local
  21. // archive. progressReader is only used if remoteURL is actually a URL
  22. // (not empty, and not a Git endpoint).
  23. func Detect(config backend.BuildConfig) (remote builder.Source, dockerfile *parser.Result, err error) {
  24. remoteURL := config.Options.RemoteContext
  25. dockerfilePath := config.Options.Dockerfile
  26. switch {
  27. case remoteURL == "":
  28. remote, dockerfile, err = newArchiveRemote(config.Source, dockerfilePath)
  29. case remoteURL == ClientSessionRemote:
  30. res, err := parser.Parse(config.Source)
  31. if err != nil {
  32. return nil, nil, err
  33. }
  34. return nil, res, nil
  35. case urlutil.IsGitURL(remoteURL):
  36. remote, dockerfile, err = newGitRemote(remoteURL, dockerfilePath)
  37. case urlutil.IsURL(remoteURL):
  38. remote, dockerfile, err = newURLRemote(remoteURL, dockerfilePath, config.ProgressWriter.ProgressReaderFunc)
  39. default:
  40. err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
  41. }
  42. return
  43. }
  44. func newArchiveRemote(rc io.ReadCloser, dockerfilePath string) (builder.Source, *parser.Result, error) {
  45. defer rc.Close()
  46. c, err := FromArchive(rc)
  47. if err != nil {
  48. return nil, nil, err
  49. }
  50. return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
  51. }
  52. func withDockerfileFromContext(c modifiableContext, dockerfilePath string) (builder.Source, *parser.Result, error) {
  53. df, err := openAt(c, dockerfilePath)
  54. if err != nil {
  55. if os.IsNotExist(err) {
  56. if dockerfilePath == builder.DefaultDockerfileName {
  57. lowercase := strings.ToLower(dockerfilePath)
  58. if _, err := StatAt(c, lowercase); err == nil {
  59. return withDockerfileFromContext(c, lowercase)
  60. }
  61. }
  62. return nil, nil, errors.Errorf("Cannot locate specified Dockerfile: %s", dockerfilePath) // backwards compatible error
  63. }
  64. c.Close()
  65. return nil, nil, err
  66. }
  67. res, err := readAndParseDockerfile(dockerfilePath, df)
  68. if err != nil {
  69. return nil, nil, err
  70. }
  71. df.Close()
  72. if err := removeDockerfile(c, dockerfilePath); err != nil {
  73. c.Close()
  74. return nil, nil, err
  75. }
  76. return c, res, nil
  77. }
  78. func newGitRemote(gitURL string, dockerfilePath string) (builder.Source, *parser.Result, error) {
  79. c, err := MakeGitContext(gitURL) // TODO: change this to NewLazySource
  80. if err != nil {
  81. return nil, nil, err
  82. }
  83. return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
  84. }
  85. func newURLRemote(url string, dockerfilePath string, progressReader func(in io.ReadCloser) io.ReadCloser) (builder.Source, *parser.Result, error) {
  86. var dockerfile io.ReadCloser
  87. dockerfileFoundErr := errors.New("found-dockerfile")
  88. c, err := MakeRemoteContext(url, map[string]func(io.ReadCloser) (io.ReadCloser, error){
  89. mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
  90. dockerfile = rc
  91. return nil, dockerfileFoundErr
  92. },
  93. // fallback handler (tar context)
  94. "": func(rc io.ReadCloser) (io.ReadCloser, error) {
  95. return progressReader(rc), nil
  96. },
  97. })
  98. switch {
  99. case err == dockerfileFoundErr:
  100. res, err := parser.Parse(dockerfile)
  101. return nil, res, err
  102. case err != nil:
  103. return nil, nil, err
  104. }
  105. return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
  106. }
  107. func removeDockerfile(c modifiableContext, filesToRemove ...string) error {
  108. f, err := openAt(c, ".dockerignore")
  109. // Note that a missing .dockerignore file isn't treated as an error
  110. switch {
  111. case os.IsNotExist(err):
  112. return nil
  113. case err != nil:
  114. return err
  115. }
  116. excludes, err := dockerignore.ReadAll(f)
  117. if err != nil {
  118. f.Close()
  119. return err
  120. }
  121. f.Close()
  122. filesToRemove = append([]string{".dockerignore"}, filesToRemove...)
  123. for _, fileToRemove := range filesToRemove {
  124. if rm, _ := fileutils.Matches(fileToRemove, excludes); rm {
  125. if err := c.Remove(fileToRemove); err != nil {
  126. logrus.Errorf("failed to remove %s: %v", fileToRemove, err)
  127. }
  128. }
  129. }
  130. return nil
  131. }
  132. func readAndParseDockerfile(name string, rc io.Reader) (*parser.Result, error) {
  133. br := bufio.NewReader(rc)
  134. if _, err := br.Peek(1); err != nil {
  135. if err == io.EOF {
  136. return nil, errors.Errorf("the Dockerfile (%s) cannot be empty", name)
  137. }
  138. return nil, errors.Wrap(err, "unexpected error reading Dockerfile")
  139. }
  140. return parser.Parse(br)
  141. }
  142. func openAt(remote builder.Source, path string) (driver.File, error) {
  143. fullPath, err := FullPath(remote, path)
  144. if err != nil {
  145. return nil, err
  146. }
  147. return remote.Root().Open(fullPath)
  148. }
  149. // StatAt is a helper for calling Stat on a path from a source
  150. func StatAt(remote builder.Source, path string) (os.FileInfo, error) {
  151. fullPath, err := FullPath(remote, path)
  152. if err != nil {
  153. return nil, err
  154. }
  155. return remote.Root().Stat(fullPath)
  156. }
  157. // FullPath is a helper for getting a full path for a path from a source
  158. func FullPath(remote builder.Source, path string) (string, error) {
  159. fullPath, err := remote.Root().ResolveScopedPath(path, true)
  160. if err != nil {
  161. return "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error
  162. }
  163. return fullPath, nil
  164. }