builder/remotecontext/detect.go
d1faf3df
 package remotecontext
 
 import (
 	"bufio"
 	"fmt"
 	"io"
 	"os"
 	"path/filepath"
 	"strings"
 
0296797f
 	"github.com/docker/docker/api/types/backend"
d1faf3df
 	"github.com/docker/docker/builder"
 	"github.com/docker/docker/builder/dockerfile/parser"
 	"github.com/docker/docker/builder/dockerignore"
 	"github.com/docker/docker/pkg/fileutils"
 	"github.com/docker/docker/pkg/symlink"
 	"github.com/docker/docker/pkg/urlutil"
 	"github.com/pkg/errors"
1009e6a4
 	"github.com/sirupsen/logrus"
d1faf3df
 )
 
5c3d2d55
 // ClientSessionRemote is identifier for client-session context transport
 const ClientSessionRemote = "client-session"
 
d1faf3df
 // Detect returns a context and dockerfile from remote location or local
 // archive. progressReader is only used if remoteURL is actually a URL
 // (not empty, and not a Git endpoint).
0296797f
 func Detect(config backend.BuildConfig) (remote builder.Source, dockerfile *parser.Result, err error) {
 	remoteURL := config.Options.RemoteContext
 	dockerfilePath := config.Options.Dockerfile
 
d1faf3df
 	switch {
 	case remoteURL == "":
0296797f
 		remote, dockerfile, err = newArchiveRemote(config.Source, dockerfilePath)
5c3d2d55
 	case remoteURL == ClientSessionRemote:
 		res, err := parser.Parse(config.Source)
 		if err != nil {
 			return nil, nil, err
 		}
 		return nil, res, nil
d1faf3df
 	case urlutil.IsGitURL(remoteURL):
 		remote, dockerfile, err = newGitRemote(remoteURL, dockerfilePath)
 	case urlutil.IsURL(remoteURL):
0296797f
 		remote, dockerfile, err = newURLRemote(remoteURL, dockerfilePath, config.ProgressWriter.ProgressReaderFunc)
d1faf3df
 	default:
 		err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
 	}
 	return
 }
 
 func newArchiveRemote(rc io.ReadCloser, dockerfilePath string) (builder.Source, *parser.Result, error) {
23628bd7
 	defer rc.Close()
5c3d2d55
 	c, err := FromArchive(rc)
d1faf3df
 	if err != nil {
 		return nil, nil, err
 	}
 
 	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
 }
 
 func withDockerfileFromContext(c modifiableContext, dockerfilePath string) (builder.Source, *parser.Result, error) {
 	df, err := openAt(c, dockerfilePath)
 	if err != nil {
 		if os.IsNotExist(err) {
 			if dockerfilePath == builder.DefaultDockerfileName {
 				lowercase := strings.ToLower(dockerfilePath)
 				if _, err := StatAt(c, lowercase); err == nil {
 					return withDockerfileFromContext(c, lowercase)
 				}
 			}
 			return nil, nil, errors.Errorf("Cannot locate specified Dockerfile: %s", dockerfilePath) // backwards compatible error
 		}
 		c.Close()
 		return nil, nil, err
 	}
 
 	res, err := readAndParseDockerfile(dockerfilePath, df)
 	if err != nil {
 		return nil, nil, err
 	}
 
 	df.Close()
 
 	if err := removeDockerfile(c, dockerfilePath); err != nil {
 		c.Close()
 		return nil, nil, err
 	}
 
 	return c, res, nil
 }
 
 func newGitRemote(gitURL string, dockerfilePath string) (builder.Source, *parser.Result, error) {
ecd44d23
 	c, err := MakeGitContext(gitURL) // TODO: change this to NewLazySource
d1faf3df
 	if err != nil {
 		return nil, nil, err
 	}
 	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
 }
 
 func newURLRemote(url string, dockerfilePath string, progressReader func(in io.ReadCloser) io.ReadCloser) (builder.Source, *parser.Result, error) {
 	var dockerfile io.ReadCloser
 	dockerfileFoundErr := errors.New("found-dockerfile")
 	c, err := MakeRemoteContext(url, map[string]func(io.ReadCloser) (io.ReadCloser, error){
c91521be
 		mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) {
d1faf3df
 			dockerfile = rc
 			return nil, dockerfileFoundErr
 		},
 		// fallback handler (tar context)
 		"": func(rc io.ReadCloser) (io.ReadCloser, error) {
 			return progressReader(rc), nil
 		},
 	})
b47b375c
 	switch {
 	case err == dockerfileFoundErr:
 		res, err := parser.Parse(dockerfile)
 		return nil, res, err
 	case err != nil:
d1faf3df
 		return nil, nil, err
 	}
 	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
 }
 
 func removeDockerfile(c modifiableContext, filesToRemove ...string) error {
 	f, err := openAt(c, ".dockerignore")
 	// Note that a missing .dockerignore file isn't treated as an error
 	switch {
 	case os.IsNotExist(err):
 		return nil
 	case err != nil:
 		return err
 	}
 	excludes, err := dockerignore.ReadAll(f)
0d99cb86
 	if err != nil {
3072fce0
 		f.Close()
0d99cb86
 		return err
 	}
d1faf3df
 	f.Close()
 	filesToRemove = append([]string{".dockerignore"}, filesToRemove...)
 	for _, fileToRemove := range filesToRemove {
 		if rm, _ := fileutils.Matches(fileToRemove, excludes); rm {
 			if err := c.Remove(fileToRemove); err != nil {
 				logrus.Errorf("failed to remove %s: %v", fileToRemove, err)
 			}
 		}
 	}
 	return nil
 }
 
 func readAndParseDockerfile(name string, rc io.Reader) (*parser.Result, error) {
 	br := bufio.NewReader(rc)
 	if _, err := br.Peek(1); err != nil {
 		if err == io.EOF {
 			return nil, errors.Errorf("the Dockerfile (%s) cannot be empty", name)
 		}
 		return nil, errors.Wrap(err, "unexpected error reading Dockerfile")
 	}
 	return parser.Parse(br)
 }
 
 func openAt(remote builder.Source, path string) (*os.File, error) {
 	fullPath, err := FullPath(remote, path)
 	if err != nil {
 		return nil, err
 	}
 	return os.Open(fullPath)
 }
 
 // StatAt is a helper for calling Stat on a path from a source
 func StatAt(remote builder.Source, path string) (os.FileInfo, error) {
 	fullPath, err := FullPath(remote, path)
 	if err != nil {
 		return nil, err
 	}
 	return os.Stat(fullPath)
 }
 
 // FullPath is a helper for getting a full path for a path from a source
 func FullPath(remote builder.Source, path string) (string, error) {
 	fullPath, err := symlink.FollowSymlinkInScope(filepath.Join(remote.Root(), path), remote.Root())
 	if err != nil {
 		return "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error
 	}
 	return fullPath, nil
 }