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  }