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  }