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