detect.go 5.8 KB

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