| package remotecontext |
| |
| import ( |
| "bufio" |
| "fmt" |
| "io" |
| "os" |
| "strings" |
| |
| "github.com/containerd/continuity/driver" |
| "github.com/docker/docker/api/types/backend" |
| "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/urlutil" |
| "github.com/pkg/errors" |
| "github.com/sirupsen/logrus" |
| ) |
| |
| // ClientSessionRemote is identifier for client-session context transport |
| const ClientSessionRemote = "client-session" |
| |
| // 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). |
| func Detect(config backend.BuildConfig) (remote builder.Source, dockerfile *parser.Result, err error) { |
| remoteURL := config.Options.RemoteContext |
| dockerfilePath := config.Options.Dockerfile |
| |
| switch { |
| case remoteURL == "": |
| remote, dockerfile, err = newArchiveRemote(config.Source, dockerfilePath) |
| case remoteURL == ClientSessionRemote: |
| res, err := parser.Parse(config.Source) |
| if err != nil { |
| return nil, nil, err |
| } |
| return nil, res, nil |
| case urlutil.IsGitURL(remoteURL): |
| remote, dockerfile, err = newGitRemote(remoteURL, dockerfilePath) |
| case urlutil.IsURL(remoteURL): |
| remote, dockerfile, err = newURLRemote(remoteURL, dockerfilePath, config.ProgressWriter.ProgressReaderFunc) |
| 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) { |
| defer rc.Close() |
| c, err := FromArchive(rc) |
| 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) { |
| c, err := MakeGitContext(gitURL) // TODO: change this to NewLazySource |
| 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){ |
| mimeTypes.TextPlain: func(rc io.ReadCloser) (io.ReadCloser, error) { |
| dockerfile = rc |
| return nil, dockerfileFoundErr |
| }, |
| // fallback handler (tar context) |
| "": func(rc io.ReadCloser) (io.ReadCloser, error) { |
| return progressReader(rc), nil |
| }, |
| }) |
| switch { |
| case err == dockerfileFoundErr: |
| res, err := parser.Parse(dockerfile) |
| return nil, res, err |
| case err != nil: |
| 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) |
| if err != nil { |
| f.Close() |
| return err |
| } |
| 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) (driver.File, error) { |
| fullPath, err := FullPath(remote, path) |
| if err != nil { |
| return nil, err |
| } |
| return remote.Root().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 remote.Root().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 := remote.Root().ResolveScopedPath(path, true) |
| if err != nil { |
| return "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error |
| } |
| return fullPath, nil |
| } |