github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/command/image/build/context.go (about) 1 package build 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/docker/docker/builder/remotecontext/git" 18 "github.com/docker/docker/pkg/archive" 19 "github.com/docker/docker/pkg/fileutils" 20 "github.com/docker/docker/pkg/ioutils" 21 "github.com/docker/docker/pkg/pools" 22 "github.com/docker/docker/pkg/progress" 23 "github.com/docker/docker/pkg/streamformatter" 24 "github.com/docker/docker/pkg/stringid" 25 "github.com/pkg/errors" 26 exec "golang.org/x/sys/execabs" 27 ) 28 29 const ( 30 // DefaultDockerfileName is the Default filename with Docker commands, read by docker build 31 DefaultDockerfileName string = "Dockerfile" 32 // archiveHeaderSize is the number of bytes in an archive header 33 archiveHeaderSize = 512 34 ) 35 36 // ValidateContextDirectory checks if all the contents of the directory 37 // can be read and returns an error if some files can't be read 38 // symlinks which point to non-existing files don't trigger an error 39 func ValidateContextDirectory(srcPath string, excludes []string) error { 40 contextRoot, err := getContextRoot(srcPath) 41 if err != nil { 42 return err 43 } 44 45 pm, err := fileutils.NewPatternMatcher(excludes) 46 if err != nil { 47 return err 48 } 49 50 return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { 51 if err != nil { 52 if os.IsPermission(err) { 53 return errors.Errorf("can't stat '%s'", filePath) 54 } 55 if os.IsNotExist(err) { 56 return errors.Errorf("file ('%s') not found or excluded by .dockerignore", filePath) 57 } 58 return err 59 } 60 61 // skip this directory/file if it's not in the path, it won't get added to the context 62 if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { 63 return err 64 } else if skip, err := filepathMatches(pm, relFilePath); err != nil { 65 return err 66 } else if skip { 67 if f.IsDir() { 68 return filepath.SkipDir 69 } 70 return nil 71 } 72 73 // skip checking if symlinks point to non-existing files, such symlinks can be useful 74 // also skip named pipes, because they hanging on open 75 if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { 76 return nil 77 } 78 79 if !f.IsDir() { 80 currentFile, err := os.Open(filePath) 81 if err != nil && os.IsPermission(err) { 82 return errors.Errorf("no permission to read from '%s'", filePath) 83 } 84 currentFile.Close() 85 } 86 return nil 87 }) 88 } 89 90 func filepathMatches(matcher *fileutils.PatternMatcher, file string) (bool, error) { 91 file = filepath.Clean(file) 92 if file == "." { 93 // Don't let them exclude everything, kind of silly. 94 return false, nil 95 } 96 return matcher.Matches(file) 97 } 98 99 // DetectArchiveReader detects whether the input stream is an archive or a 100 // Dockerfile and returns a buffered version of input, safe to consume in lieu 101 // of input. If an archive is detected, isArchive is set to true, and to false 102 // otherwise, in which case it is safe to assume input represents the contents 103 // of a Dockerfile. 104 func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, err error) { 105 buf := bufio.NewReader(input) 106 107 magic, err := buf.Peek(archiveHeaderSize * 2) 108 if err != nil && err != io.EOF { 109 return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err) 110 } 111 112 return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil 113 } 114 115 // WriteTempDockerfile writes a Dockerfile stream to a temporary file with a 116 // name specified by DefaultDockerfileName and returns the path to the 117 // temporary directory containing the Dockerfile. 118 func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { 119 // err is a named return value, due to the defer call below. 120 dockerfileDir, err = ioutil.TempDir("", "docker-build-tempdockerfile-") 121 if err != nil { 122 return "", errors.Errorf("unable to create temporary context directory: %v", err) 123 } 124 defer func() { 125 if err != nil { 126 os.RemoveAll(dockerfileDir) 127 } 128 }() 129 130 f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName)) 131 if err != nil { 132 return "", err 133 } 134 defer f.Close() 135 if _, err := io.Copy(f, rc); err != nil { 136 return "", err 137 } 138 return dockerfileDir, rc.Close() 139 } 140 141 // GetContextFromReader will read the contents of the given reader as either a 142 // Dockerfile or tar archive. Returns a tar archive used as a context and a 143 // path to the Dockerfile inside the tar. 144 func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { 145 rc, isArchive, err := DetectArchiveReader(rc) 146 if err != nil { 147 return nil, "", err 148 } 149 150 if isArchive { 151 return rc, dockerfileName, nil 152 } 153 154 // Input should be read as a Dockerfile. 155 156 if dockerfileName == "-" { 157 return nil, "", errors.New("build context is not an archive") 158 } 159 160 dockerfileDir, err := WriteTempDockerfile(rc) 161 if err != nil { 162 return nil, "", err 163 } 164 165 tar, err := archive.Tar(dockerfileDir, archive.Uncompressed) 166 if err != nil { 167 return nil, "", err 168 } 169 170 return ioutils.NewReadCloserWrapper(tar, func() error { 171 err := tar.Close() 172 os.RemoveAll(dockerfileDir) 173 return err 174 }), DefaultDockerfileName, nil 175 } 176 177 // IsArchive checks for the magic bytes of a tar or any supported compression 178 // algorithm. 179 func IsArchive(header []byte) bool { 180 compression := archive.DetectCompression(header) 181 if compression != archive.Uncompressed { 182 return true 183 } 184 r := tar.NewReader(bytes.NewBuffer(header)) 185 _, err := r.Next() 186 return err == nil 187 } 188 189 // GetContextFromGitURL uses a Git URL as context for a `docker build`. The 190 // git repo is cloned into a temporary directory used as the context directory. 191 // Returns the absolute path to the temporary context directory, the relative 192 // path of the dockerfile in that context directory, and a non-nil error on 193 // success. 194 func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) { 195 if _, err := exec.LookPath("git"); err != nil { 196 return "", "", errors.Wrapf(err, "unable to find 'git'") 197 } 198 absContextDir, err := git.Clone(gitURL) 199 if err != nil { 200 return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory") 201 } 202 203 absContextDir, err = ResolveAndValidateContextPath(absContextDir) 204 if err != nil { 205 return "", "", err 206 } 207 relDockerfile, err := getDockerfileRelPath(absContextDir, dockerfileName) 208 if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { 209 return "", "", errors.Errorf("the Dockerfile (%s) must be within the build context", dockerfileName) 210 } 211 212 return absContextDir, relDockerfile, err 213 } 214 215 // GetContextFromURL uses a remote URL as context for a `docker build`. The 216 // remote resource is downloaded as either a Dockerfile or a tar archive. 217 // Returns the tar archive used for the context and a path of the 218 // dockerfile inside the tar. 219 func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { 220 response, err := getWithStatusError(remoteURL) 221 if err != nil { 222 return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) 223 } 224 progressOutput := streamformatter.NewProgressOutput(out) 225 226 // Pass the response body through a progress reader. 227 progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) 228 229 return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) 230 } 231 232 // getWithStatusError does an http.Get() and returns an error if the 233 // status code is 4xx or 5xx. 234 func getWithStatusError(url string) (resp *http.Response, err error) { 235 // #nosec G107 236 if resp, err = http.Get(url); err != nil { 237 return nil, err 238 } 239 if resp.StatusCode < 400 { 240 return resp, nil 241 } 242 msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status) 243 body, err := ioutil.ReadAll(resp.Body) 244 resp.Body.Close() 245 if err != nil { 246 return nil, errors.Wrapf(err, "%s: error reading body", msg) 247 } 248 return nil, errors.Errorf("%s: %s", msg, bytes.TrimSpace(body)) 249 } 250 251 // GetContextFromLocalDir uses the given local directory as context for a 252 // `docker build`. Returns the absolute path to the local context directory, 253 // the relative path of the dockerfile in that context directory, and a non-nil 254 // error on success. 255 func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, error) { 256 localDir, err := ResolveAndValidateContextPath(localDir) 257 if err != nil { 258 return "", "", err 259 } 260 261 // When using a local context directory, and the Dockerfile is specified 262 // with the `-f/--file` option then it is considered relative to the 263 // current directory and not the context directory. 264 if dockerfileName != "" && dockerfileName != "-" { 265 if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { 266 return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) 267 } 268 } 269 270 relDockerfile, err := getDockerfileRelPath(localDir, dockerfileName) 271 return localDir, relDockerfile, err 272 } 273 274 // ResolveAndValidateContextPath uses the given context directory for a `docker build` 275 // and returns the absolute path to the context directory. 276 func ResolveAndValidateContextPath(givenContextDir string) (string, error) { 277 absContextDir, err := filepath.Abs(givenContextDir) 278 if err != nil { 279 return "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) 280 } 281 282 // The context dir might be a symbolic link, so follow it to the actual 283 // target directory. 284 // 285 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 286 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 287 // paths (those starting with \\). This hack means that when using links 288 // on UNC paths, they will not be followed. 289 if !isUNC(absContextDir) { 290 absContextDir, err = filepath.EvalSymlinks(absContextDir) 291 if err != nil { 292 return "", errors.Errorf("unable to evaluate symlinks in context path: %v", err) 293 } 294 } 295 296 stat, err := os.Lstat(absContextDir) 297 if err != nil { 298 return "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err) 299 } 300 301 if !stat.IsDir() { 302 return "", errors.Errorf("context must be a directory: %s", absContextDir) 303 } 304 return absContextDir, err 305 } 306 307 // getDockerfileRelPath returns the dockerfile path relative to the context 308 // directory 309 func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error) { 310 var err error 311 312 if givenDockerfile == "-" { 313 return givenDockerfile, nil 314 } 315 316 absDockerfile := givenDockerfile 317 if absDockerfile == "" { 318 // No -f/--file was specified so use the default relative to the 319 // context directory. 320 absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) 321 322 // Just to be nice ;-) look for 'dockerfile' too but only 323 // use it if we found it, otherwise ignore this check 324 if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { 325 altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) 326 if _, err = os.Lstat(altPath); err == nil { 327 absDockerfile = altPath 328 } 329 } 330 } 331 332 // If not already an absolute path, the Dockerfile path should be joined to 333 // the base directory. 334 if !filepath.IsAbs(absDockerfile) { 335 absDockerfile = filepath.Join(absContextDir, absDockerfile) 336 } 337 338 // Evaluate symlinks in the path to the Dockerfile too. 339 // 340 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 341 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 342 // paths (those starting with \\). This hack means that when using links 343 // on UNC paths, they will not be followed. 344 if !isUNC(absDockerfile) { 345 absDockerfile, err = filepath.EvalSymlinks(absDockerfile) 346 if err != nil { 347 return "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) 348 } 349 } 350 351 if _, err := os.Lstat(absDockerfile); err != nil { 352 if os.IsNotExist(err) { 353 return "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) 354 } 355 return "", errors.Errorf("unable to stat Dockerfile: %v", err) 356 } 357 358 relDockerfile, err := filepath.Rel(absContextDir, absDockerfile) 359 if err != nil { 360 return "", errors.Errorf("unable to get relative Dockerfile path: %v", err) 361 } 362 363 return relDockerfile, nil 364 } 365 366 // isUNC returns true if the path is UNC (one starting \\). It always returns 367 // false on Linux. 368 func isUNC(path string) bool { 369 return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) 370 } 371 372 // AddDockerfileToBuildContext from a ReadCloser, returns a new archive and 373 // the relative path to the dockerfile in the context. 374 func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { 375 file, err := ioutil.ReadAll(dockerfileCtx) 376 dockerfileCtx.Close() 377 if err != nil { 378 return nil, "", err 379 } 380 now := time.Now() 381 hdrTmpl := &tar.Header{ 382 Mode: 0600, 383 Uid: 0, 384 Gid: 0, 385 ModTime: now, 386 Typeflag: tar.TypeReg, 387 AccessTime: now, 388 ChangeTime: now, 389 } 390 randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] 391 392 buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ 393 // Add the dockerfile with a random filename 394 randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { 395 return hdrTmpl, file, nil 396 }, 397 // Update .dockerignore to include the random filename 398 ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { 399 if h == nil { 400 h = hdrTmpl 401 } 402 403 b := &bytes.Buffer{} 404 if content != nil { 405 if _, err := b.ReadFrom(content); err != nil { 406 return nil, nil, err 407 } 408 } else { 409 b.WriteString(".dockerignore") 410 } 411 b.WriteString("\n" + randomName + "\n") 412 return h, b.Bytes(), nil 413 }, 414 }) 415 return buildCtx, randomName, nil 416 } 417 418 // Compress the build context for sending to the API 419 func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { 420 pipeReader, pipeWriter := io.Pipe() 421 422 go func() { 423 compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip) 424 if err != nil { 425 pipeWriter.CloseWithError(err) 426 } 427 defer buildCtx.Close() 428 429 if _, err := pools.Copy(compressWriter, buildCtx); err != nil { 430 pipeWriter.CloseWithError( 431 errors.Wrap(err, "failed to compress context")) 432 compressWriter.Close() 433 return 434 } 435 compressWriter.Close() 436 pipeWriter.Close() 437 }() 438 439 return pipeReader, nil 440 }