github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/cli/command/image/build/context.go (about) 1 package build 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "runtime" 12 "strings" 13 14 "github.com/docker/docker/pkg/archive" 15 "github.com/docker/docker/pkg/fileutils" 16 "github.com/docker/docker/pkg/gitutils" 17 "github.com/docker/docker/pkg/httputils" 18 "github.com/docker/docker/pkg/ioutils" 19 "github.com/docker/docker/pkg/progress" 20 "github.com/docker/docker/pkg/streamformatter" 21 ) 22 23 const ( 24 // DefaultDockerfileName is the Default filename with Docker commands, read by docker build 25 DefaultDockerfileName string = "Dockerfile" 26 ) 27 28 // ValidateContextDirectory checks if all the contents of the directory 29 // can be read and returns an error if some files can't be read 30 // symlinks which point to non-existing files don't trigger an error 31 func ValidateContextDirectory(srcPath string, excludes []string) error { 32 contextRoot, err := getContextRoot(srcPath) 33 if err != nil { 34 return err 35 } 36 return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { 37 if err != nil { 38 if os.IsPermission(err) { 39 return fmt.Errorf("can't stat '%s'", filePath) 40 } 41 if os.IsNotExist(err) { 42 return nil 43 } 44 return err 45 } 46 47 // skip this directory/file if it's not in the path, it won't get added to the context 48 if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { 49 return err 50 } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { 51 return err 52 } else if skip { 53 if f.IsDir() { 54 return filepath.SkipDir 55 } 56 return nil 57 } 58 59 // skip checking if symlinks point to non-existing files, such symlinks can be useful 60 // also skip named pipes, because they hanging on open 61 if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { 62 return nil 63 } 64 65 if !f.IsDir() { 66 currentFile, err := os.Open(filePath) 67 if err != nil && os.IsPermission(err) { 68 return fmt.Errorf("no permission to read from '%s'", filePath) 69 } 70 currentFile.Close() 71 } 72 return nil 73 }) 74 } 75 76 // GetContextFromReader will read the contents of the given reader as either a 77 // Dockerfile or tar archive. Returns a tar archive used as a context and a 78 // path to the Dockerfile inside the tar. 79 func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { 80 buf := bufio.NewReader(r) 81 82 magic, err := buf.Peek(archive.HeaderSize) 83 if err != nil && err != io.EOF { 84 return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err) 85 } 86 87 if archive.IsArchive(magic) { 88 return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil 89 } 90 91 // Input should be read as a Dockerfile. 92 tmpDir, err := ioutil.TempDir("", "docker-build-context-") 93 if err != nil { 94 return nil, "", fmt.Errorf("unable to create temporary context directory: %v", err) 95 } 96 97 f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) 98 if err != nil { 99 return nil, "", err 100 } 101 _, err = io.Copy(f, buf) 102 if err != nil { 103 f.Close() 104 return nil, "", err 105 } 106 107 if err := f.Close(); err != nil { 108 return nil, "", err 109 } 110 if err := r.Close(); err != nil { 111 return nil, "", err 112 } 113 114 tar, err := archive.Tar(tmpDir, archive.Uncompressed) 115 if err != nil { 116 return nil, "", err 117 } 118 119 return ioutils.NewReadCloserWrapper(tar, func() error { 120 err := tar.Close() 121 os.RemoveAll(tmpDir) 122 return err 123 }), DefaultDockerfileName, nil 124 125 } 126 127 // GetContextFromGitURL uses a Git URL as context for a `docker build`. The 128 // git repo is cloned into a temporary directory used as the context directory. 129 // Returns the absolute path to the temporary context directory, the relative 130 // path of the dockerfile in that context directory, and a non-nil error on 131 // success. 132 func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { 133 if _, err := exec.LookPath("git"); err != nil { 134 return "", "", fmt.Errorf("unable to find 'git': %v", err) 135 } 136 if absContextDir, err = gitutils.Clone(gitURL); err != nil { 137 return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %v", err) 138 } 139 140 return getDockerfileRelPath(absContextDir, dockerfileName) 141 } 142 143 // GetContextFromURL uses a remote URL as context for a `docker build`. The 144 // remote resource is downloaded as either a Dockerfile or a tar archive. 145 // Returns the tar archive used for the context and a path of the 146 // dockerfile inside the tar. 147 func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { 148 response, err := httputils.Download(remoteURL) 149 if err != nil { 150 return nil, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) 151 } 152 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) 153 154 // Pass the response body through a progress reader. 155 progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) 156 157 return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) 158 } 159 160 // GetContextFromLocalDir uses the given local directory as context for a 161 // `docker build`. Returns the absolute path to the local context directory, 162 // the relative path of the dockerfile in that context directory, and a non-nil 163 // error on success. 164 func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) { 165 // When using a local context directory, when the Dockerfile is specified 166 // with the `-f/--file` option then it is considered relative to the 167 // current directory and not the context directory. 168 if dockerfileName != "" { 169 if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { 170 return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %v", err) 171 } 172 } 173 174 return getDockerfileRelPath(localDir, dockerfileName) 175 } 176 177 // getDockerfileRelPath uses the given context directory for a `docker build` 178 // and returns the absolute path to the context directory, the relative path of 179 // the dockerfile in that context directory, and a non-nil error on success. 180 func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { 181 if absContextDir, err = filepath.Abs(givenContextDir); err != nil { 182 return "", "", fmt.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) 183 } 184 185 // The context dir might be a symbolic link, so follow it to the actual 186 // target directory. 187 // 188 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 189 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 190 // paths (those starting with \\). This hack means that when using links 191 // on UNC paths, they will not be followed. 192 if !isUNC(absContextDir) { 193 absContextDir, err = filepath.EvalSymlinks(absContextDir) 194 if err != nil { 195 return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) 196 } 197 } 198 199 stat, err := os.Lstat(absContextDir) 200 if err != nil { 201 return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) 202 } 203 204 if !stat.IsDir() { 205 return "", "", fmt.Errorf("context must be a directory: %s", absContextDir) 206 } 207 208 absDockerfile := givenDockerfile 209 if absDockerfile == "" { 210 // No -f/--file was specified so use the default relative to the 211 // context directory. 212 absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) 213 214 // Just to be nice ;-) look for 'dockerfile' too but only 215 // use it if we found it, otherwise ignore this check 216 if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { 217 altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) 218 if _, err = os.Lstat(altPath); err == nil { 219 absDockerfile = altPath 220 } 221 } 222 } 223 224 // If not already an absolute path, the Dockerfile path should be joined to 225 // the base directory. 226 if !filepath.IsAbs(absDockerfile) { 227 absDockerfile = filepath.Join(absContextDir, absDockerfile) 228 } 229 230 // Evaluate symlinks in the path to the Dockerfile too. 231 // 232 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 233 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 234 // paths (those starting with \\). This hack means that when using links 235 // on UNC paths, they will not be followed. 236 if !isUNC(absDockerfile) { 237 absDockerfile, err = filepath.EvalSymlinks(absDockerfile) 238 if err != nil { 239 return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) 240 } 241 } 242 243 if _, err := os.Lstat(absDockerfile); err != nil { 244 if os.IsNotExist(err) { 245 return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) 246 } 247 return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) 248 } 249 250 if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { 251 return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) 252 } 253 254 if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { 255 return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) 256 } 257 258 return absContextDir, relDockerfile, nil 259 } 260 261 // isUNC returns true if the path is UNC (one starting \\). It always returns 262 // false on Linux. 263 func isUNC(path string) bool { 264 return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) 265 }