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