Avoid extracting to temp directory on building from tar

Fixes #15785

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2016-01-12 13:18:24 -08:00
parent 1393c450cd
commit 42961a66a5
1 changed files with 102 additions and 130 deletions

View File

@ -22,6 +22,7 @@ import (
"github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/gitutils" "github.com/docker/docker/pkg/gitutils"
"github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/httputils"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/progress"
@ -34,6 +35,8 @@ import (
"github.com/docker/go-units" "github.com/docker/go-units"
) )
type translatorFunc func(reference.NamedTagged) (reference.Canonical, error)
// CmdBuild builds a new image from the source code at a given path. // CmdBuild builds a new image from the source code at a given path.
// //
// If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. // If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN.
@ -101,11 +104,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
switch { switch {
case specifiedContext == "-": case specifiedContext == "-":
tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) context, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName)
case urlutil.IsGitURL(specifiedContext) && hasGit: case urlutil.IsGitURL(specifiedContext) && hasGit:
tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName)
case urlutil.IsURL(specifiedContext): case urlutil.IsURL(specifiedContext):
tempDir, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName) context, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName)
default: default:
contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName)
} }
@ -122,6 +125,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
contextDir = tempDir contextDir = tempDir
} }
if context == nil {
// And canonicalize dockerfile name to a platform-independent one // And canonicalize dockerfile name to a platform-independent one
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
if err != nil { if err != nil {
@ -167,22 +171,13 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
if err != nil { if err != nil {
return err return err
} }
}
var resolvedTags []*resolvedTag var resolvedTags []*resolvedTag
if isTrusted() { if isTrusted() {
// Resolve the FROM lines in the Dockerfile to trusted digest references
// using Notary. On a successful build, we must tag the resolved digests
// to the original name specified in the Dockerfile.
var newDockerfile *trustedDockerfile
newDockerfile, resolvedTags, err = rewriteDockerfileFrom(filepath.Join(contextDir, relDockerfile), cli.trustedReference)
if err != nil {
return fmt.Errorf("unable to process Dockerfile: %v", err)
}
defer newDockerfile.Close()
// Wrap the tar archive to replace the Dockerfile entry with the rewritten // Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls. // Dockerfile which uses trusted pulls.
context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile) context = replaceDockerfileTarWrapper(context, relDockerfile, cli.trustedReference, &resolvedTags)
} }
// Setup an upload progress bar // Setup an upload progress bar
@ -459,41 +454,54 @@ func writeToFile(r io.Reader, filename string) error {
} }
// getContextFromReader will read the contents of the given reader as either a // getContextFromReader will read the contents of the given reader as either a
// Dockerfile or tar archive to be extracted to a temporary directory used as // Dockerfile or tar archive. Returns a tar archive used as a context and a
// the context directory. Returns the absolute path to the temporary context // path to the Dockerfile inside the tar.
// directory, the relative path of the dockerfile in that context directory, func getContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
// and a non-nil error on success.
func getContextFromReader(r io.Reader, dockerfileName string) (absContextDir, relDockerfile string, err error) {
buf := bufio.NewReader(r) buf := bufio.NewReader(r)
magic, err := buf.Peek(archive.HeaderSize) magic, err := buf.Peek(archive.HeaderSize)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return "", "", fmt.Errorf("failed to peek context header from STDIN: %v", err) return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err)
} }
if absContextDir, err = ioutil.TempDir("", "docker-build-context-"); err != nil { if archive.IsArchive(magic) {
return "", "", fmt.Errorf("unbale to create temporary context directory: %v", err) return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
} }
defer func(d string) { // Input should be read as a Dockerfile.
tmpDir, err := ioutil.TempDir("", "docker-build-context-")
if err != nil { if err != nil {
os.RemoveAll(d) return nil, "", fmt.Errorf("unbale to create temporary context directory: %v", err)
}
}(absContextDir)
if !archive.IsArchive(magic) { // Input should be read as a Dockerfile.
// -f option has no meaning when we're reading it from stdin,
// so just use our default Dockerfile name
relDockerfile = api.DefaultDockerfileName
return absContextDir, relDockerfile, writeToFile(buf, filepath.Join(absContextDir, relDockerfile))
} }
if err := archive.Untar(buf, absContextDir, nil); err != nil { f, err := os.Create(filepath.Join(tmpDir, api.DefaultDockerfileName))
return "", "", fmt.Errorf("unable to extract stdin to temporary context directory: %v", err) if err != nil {
return nil, "", err
}
_, err = io.Copy(f, buf)
if err != nil {
f.Close()
return nil, "", err
} }
return getDockerfileRelPath(absContextDir, dockerfileName) if err := f.Close(); err != nil {
return nil, "", err
}
if err := r.Close(); err != nil {
return nil, "", err
}
tar, err := archive.Tar(tmpDir, archive.Uncompressed)
if err != nil {
return nil, "", err
}
return ioutils.NewReadCloserWrapper(tar, func() error {
err := tar.Close()
os.RemoveAll(tmpDir)
return err
}), api.DefaultDockerfileName, nil
} }
// getContextFromGitURL uses a Git URL as context for a `docker build`. The // getContextFromGitURL uses a Git URL as context for a `docker build`. The
@ -510,23 +518,20 @@ func getContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDock
} }
// getContextFromURL uses a remote URL as context for a `docker build`. The // getContextFromURL uses a remote URL as context for a `docker build`. The
// remote resource is downloaded as either a Dockerfile or a context tar // remote resource is downloaded as either a Dockerfile or a tar archive.
// archive and stored in a temporary directory used as the context directory. // Returns the tar archive used for the context and a path of the
// Returns the absolute path to the temporary context directory, the relative // dockerfile inside the tar.
// path of the dockerfile in that context directory, and a non-nil error on func getContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) {
// success.
func getContextFromURL(out io.Writer, remoteURL, dockerfileName string) (absContextDir, relDockerfile string, err error) {
response, err := httputils.Download(remoteURL) response, err := httputils.Download(remoteURL)
if err != nil { if err != nil {
return "", "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) return nil, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err)
} }
defer response.Body.Close()
progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true)
// Pass the response body through a progress reader. // Pass the response body through a progress reader.
progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL))
return getContextFromReader(progReader, dockerfileName) return getContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
} }
// getContextFromLocalDir uses the given local directory as context for a // getContextFromLocalDir uses the given local directory as context for a
@ -548,16 +553,6 @@ func getContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel
var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
type trustedDockerfile struct {
*os.File
size int64
}
func (td *trustedDockerfile) Close() error {
td.File.Close()
return os.Remove(td.File.Name())
}
// resolvedTag records the repository, tag, and resolved digest reference // resolvedTag records the repository, tag, and resolved digest reference
// from a Dockerfile rewrite. // from a Dockerfile rewrite.
type resolvedTag struct { type resolvedTag struct {
@ -569,32 +564,9 @@ type resolvedTag struct {
// "FROM <image>" instructions to a digest reference. `translator` is a // "FROM <image>" instructions to a digest reference. `translator` is a
// function that takes a repository name and tag reference and returns a // function that takes a repository name and tag reference and returns a
// trusted digest reference. // trusted digest reference.
func rewriteDockerfileFrom(dockerfileName string, translator func(reference.NamedTagged) (reference.Canonical, error)) (newDockerfile *trustedDockerfile, resolvedTags []*resolvedTag, err error) { func rewriteDockerfileFrom(dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) {
dockerfile, err := os.Open(dockerfileName)
if err != nil {
return nil, nil, fmt.Errorf("unable to open Dockerfile: %v", err)
}
defer dockerfile.Close()
scanner := bufio.NewScanner(dockerfile) scanner := bufio.NewScanner(dockerfile)
buf := bytes.NewBuffer(nil)
// Make a tempfile to store the rewritten Dockerfile.
tempFile, err := ioutil.TempFile("", "trusted-dockerfile-")
if err != nil {
return nil, nil, fmt.Errorf("unable to make temporary trusted Dockerfile: %v", err)
}
trustedFile := &trustedDockerfile{
File: tempFile,
}
defer func() {
if err != nil {
// Close the tempfile if there was an error during Notary lookups.
// Otherwise the caller should close it.
trustedFile.Close()
}
}()
// Scan the lines of the Dockerfile, looking for a "FROM" line. // Scan the lines of the Dockerfile, looking for a "FROM" line.
for scanner.Scan() { for scanner.Scan() {
@ -622,26 +594,21 @@ func rewriteDockerfileFrom(dockerfileName string, translator func(reference.Name
} }
} }
n, err := fmt.Fprintln(tempFile, line) _, err := fmt.Fprintln(buf, line)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
trustedFile.size += int64(n)
} }
tempFile.Seek(0, os.SEEK_SET) return buf.Bytes(), resolvedTags, scanner.Err()
return trustedFile, resolvedTags, scanner.Err()
} }
// replaceDockerfileTarWrapper wraps the given input tar archive stream and // replaceDockerfileTarWrapper wraps the given input tar archive stream and
// replaces the entry with the given Dockerfile name with the contents of the // replaces the entry with the given Dockerfile name with the contents of the
// new Dockerfile. Returns a new tar archive stream with the replaced // new Dockerfile. Returns a new tar archive stream with the replaced
// Dockerfile. // Dockerfile.
func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, newDockerfile *trustedDockerfile, dockerfileName string) io.ReadCloser { func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
go func() { go func() {
tarReader := tar.NewReader(inputTarStream) tarReader := tar.NewReader(inputTarStream)
tarWriter := tar.NewWriter(pipeWriter) tarWriter := tar.NewWriter(pipeWriter)
@ -662,13 +629,18 @@ func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, newDockerfile *tr
} }
var content io.Reader = tarReader var content io.Reader = tarReader
if hdr.Name == dockerfileName { if hdr.Name == dockerfileName {
// This entry is the Dockerfile. Since the tar archive was // This entry is the Dockerfile. Since the tar archive was
// generated from a directory on the local filesystem, the // generated from a directory on the local filesystem, the
// Dockerfile will only appear once in the archive. // Dockerfile will only appear once in the archive.
hdr.Size = newDockerfile.size var newDockerfile []byte
content = newDockerfile newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(content, translator)
if err != nil {
pipeWriter.CloseWithError(err)
return
}
hdr.Size = int64(len(newDockerfile))
content = bytes.NewBuffer(newDockerfile)
} }
if err := tarWriter.WriteHeader(hdr); err != nil { if err := tarWriter.WriteHeader(hdr); err != nil {