github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/builder/remotecontext/detect.go (about)

     1  package remotecontext
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/Sirupsen/logrus"
    12  	"github.com/docker/docker/api/types/backend"
    13  	"github.com/docker/docker/builder"
    14  	"github.com/docker/docker/builder/dockerfile/parser"
    15  	"github.com/docker/docker/builder/dockerignore"
    16  	"github.com/docker/docker/pkg/fileutils"
    17  	"github.com/docker/docker/pkg/symlink"
    18  	"github.com/docker/docker/pkg/urlutil"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  // Detect returns a context and dockerfile from remote location or local
    23  // archive. progressReader is only used if remoteURL is actually a URL
    24  // (not empty, and not a Git endpoint).
    25  func Detect(config backend.BuildConfig) (remote builder.Source, dockerfile *parser.Result, err error) {
    26  	remoteURL := config.Options.RemoteContext
    27  	dockerfilePath := config.Options.Dockerfile
    28  
    29  	switch {
    30  	case remoteURL == "":
    31  		remote, dockerfile, err = newArchiveRemote(config.Source, dockerfilePath)
    32  	case urlutil.IsGitURL(remoteURL):
    33  		remote, dockerfile, err = newGitRemote(remoteURL, dockerfilePath)
    34  	case urlutil.IsURL(remoteURL):
    35  		remote, dockerfile, err = newURLRemote(remoteURL, dockerfilePath, config.ProgressWriter.ProgressReaderFunc)
    36  	default:
    37  		err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
    38  	}
    39  	return
    40  }
    41  
    42  func newArchiveRemote(rc io.ReadCloser, dockerfilePath string) (builder.Source, *parser.Result, error) {
    43  	defer rc.Close()
    44  	c, err := MakeTarSumContext(rc)
    45  	if err != nil {
    46  		return nil, nil, err
    47  	}
    48  
    49  	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
    50  }
    51  
    52  func withDockerfileFromContext(c modifiableContext, dockerfilePath string) (builder.Source, *parser.Result, error) {
    53  	df, err := openAt(c, dockerfilePath)
    54  	if err != nil {
    55  		if os.IsNotExist(err) {
    56  			if dockerfilePath == builder.DefaultDockerfileName {
    57  				lowercase := strings.ToLower(dockerfilePath)
    58  				if _, err := StatAt(c, lowercase); err == nil {
    59  					return withDockerfileFromContext(c, lowercase)
    60  				}
    61  			}
    62  			return nil, nil, errors.Errorf("Cannot locate specified Dockerfile: %s", dockerfilePath) // backwards compatible error
    63  		}
    64  		c.Close()
    65  		return nil, nil, err
    66  	}
    67  
    68  	res, err := readAndParseDockerfile(dockerfilePath, df)
    69  	if err != nil {
    70  		return nil, nil, err
    71  	}
    72  
    73  	df.Close()
    74  
    75  	if err := removeDockerfile(c, dockerfilePath); err != nil {
    76  		c.Close()
    77  		return nil, nil, err
    78  	}
    79  
    80  	return c, res, nil
    81  }
    82  
    83  func newGitRemote(gitURL string, dockerfilePath string) (builder.Source, *parser.Result, error) {
    84  	c, err := MakeGitContext(gitURL) // TODO: change this to NewLazyContext
    85  	if err != nil {
    86  		return nil, nil, err
    87  	}
    88  	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
    89  }
    90  
    91  func newURLRemote(url string, dockerfilePath string, progressReader func(in io.ReadCloser) io.ReadCloser) (builder.Source, *parser.Result, error) {
    92  	var dockerfile io.ReadCloser
    93  	dockerfileFoundErr := errors.New("found-dockerfile")
    94  	c, err := MakeRemoteContext(url, map[string]func(io.ReadCloser) (io.ReadCloser, error){
    95  		mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
    96  			dockerfile = rc
    97  			return nil, dockerfileFoundErr
    98  		},
    99  		// fallback handler (tar context)
   100  		"": func(rc io.ReadCloser) (io.ReadCloser, error) {
   101  			return progressReader(rc), nil
   102  		},
   103  	})
   104  	if err != nil {
   105  		if err == dockerfileFoundErr {
   106  			res, err := parser.Parse(dockerfile)
   107  			if err != nil {
   108  				return nil, nil, err
   109  			}
   110  			return nil, res, nil
   111  		}
   112  		return nil, nil, err
   113  	}
   114  
   115  	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
   116  }
   117  
   118  func removeDockerfile(c modifiableContext, filesToRemove ...string) error {
   119  	f, err := openAt(c, ".dockerignore")
   120  	// Note that a missing .dockerignore file isn't treated as an error
   121  	switch {
   122  	case os.IsNotExist(err):
   123  		return nil
   124  	case err != nil:
   125  		return err
   126  	}
   127  	excludes, err := dockerignore.ReadAll(f)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	f.Close()
   132  	filesToRemove = append([]string{".dockerignore"}, filesToRemove...)
   133  	for _, fileToRemove := range filesToRemove {
   134  		if rm, _ := fileutils.Matches(fileToRemove, excludes); rm {
   135  			if err := c.Remove(fileToRemove); err != nil {
   136  				logrus.Errorf("failed to remove %s: %v", fileToRemove, err)
   137  			}
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  func readAndParseDockerfile(name string, rc io.Reader) (*parser.Result, error) {
   144  	br := bufio.NewReader(rc)
   145  	if _, err := br.Peek(1); err != nil {
   146  		if err == io.EOF {
   147  			return nil, errors.Errorf("the Dockerfile (%s) cannot be empty", name)
   148  		}
   149  		return nil, errors.Wrap(err, "unexpected error reading Dockerfile")
   150  	}
   151  	return parser.Parse(br)
   152  }
   153  
   154  func openAt(remote builder.Source, path string) (*os.File, error) {
   155  	fullPath, err := FullPath(remote, path)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	return os.Open(fullPath)
   160  }
   161  
   162  // StatAt is a helper for calling Stat on a path from a source
   163  func StatAt(remote builder.Source, path string) (os.FileInfo, error) {
   164  	fullPath, err := FullPath(remote, path)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	return os.Stat(fullPath)
   169  }
   170  
   171  // FullPath is a helper for getting a full path for a path from a source
   172  func FullPath(remote builder.Source, path string) (string, error) {
   173  	fullPath, err := symlink.FollowSymlinkInScope(filepath.Join(remote.Root(), path), remote.Root())
   174  	if err != nil {
   175  		return "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error
   176  	}
   177  	return fullPath, nil
   178  }