From e0b3921a03a37c97cc6cb18964aceee9a8607cd5 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Sat, 19 May 2018 02:56:22 +0000 Subject: [PATCH] build: Add support for using context from stdin with buildkit Signed-off-by: Tibor Vass --- cli/command/image/build/context.go | 82 ++++++++++++++++++----------- cli/command/image/build_buildkit.go | 56 ++++++++++++++------ 2 files changed, 92 insertions(+), 46 deletions(-) diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index 463e3156c2..eaeac9e195 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -81,59 +81,81 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { }) } -// GetContextFromReader will read the contents of the given reader as either a -// Dockerfile or tar archive. Returns a tar archive used as a context and a -// path to the Dockerfile inside the tar. -func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { - buf := bufio.NewReader(r) +// DetectArchiveReader detects whether the input stream is an archive or a +// Dockerfile and returns a buffered version of input, safe to consume in lieu +// of input. If an archive is detected, isArchive is set to true, and to false +// otherwise, in which case it is safe to assume input represents the contents +// of a Dockerfile. +func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, err error) { + buf := bufio.NewReader(input) magic, err := buf.Peek(archiveHeaderSize) if err != nil && err != io.EOF { - return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err) + return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err) } - if IsArchive(magic) { - return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil + return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil +} + + +// WriteTempDockerfile writes a Dockerfile stream to a temporary file with a +// name specified by DefaultDockerfileName and returns the path to the +// temporary directory containing the Dockerfile. +func WriteTempDockerfile(rc io.ReadCloser) (string, error) { + dockerfileDir, err := ioutil.TempDir("", "docker-build-tempdockerfile-") + if err != nil { + return "", errors.Errorf("unable to create temporary context directory: %v", err) } + f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName)) + if err != nil { + return "", err + } + _, err = io.Copy(f, rc) + if err != nil { + f.Close() + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + return dockerfileDir, rc.Close() +} + +// GetContextFromReader will read the contents of the given reader as either a +// Dockerfile or tar archive. Returns a tar archive used as a context and a +// path to the Dockerfile inside the tar. +func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { + rc, isArchive, err := DetectArchiveReader(rc) + if err != nil { + return nil, "", err + } + + if isArchive { + return rc, dockerfileName, nil + } + + // Input should be read as a Dockerfile. + if dockerfileName == "-" { return nil, "", errors.New("build context is not an archive") } - // Input should be read as a Dockerfile. - tmpDir, err := ioutil.TempDir("", "docker-build-context-") - if err != nil { - return nil, "", errors.Errorf("unable to create temporary context directory: %v", err) - } - - f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) + dockerfileDir, err := WriteTempDockerfile(rc) if err != nil { return nil, "", err } - _, err = io.Copy(f, buf) - if err != nil { - f.Close() - return nil, "", err - } - 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) + tar, err := archive.Tar(dockerfileDir, archive.Uncompressed) if err != nil { return nil, "", err } return ioutils.NewReadCloserWrapper(tar, func() error { err := tar.Close() - os.RemoveAll(tmpDir) + os.RemoveAll(dockerfileDir) return err }), DefaultDockerfileName, nil - } // IsArchive checks for the magic bytes of a tar or any supported compression diff --git a/cli/command/image/build_buildkit.go b/cli/command/image/build_buildkit.go index 1deab0cb2d..c380bad7f6 100644 --- a/cli/command/image/build_buildkit.go +++ b/cli/command/image/build_buildkit.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "io" + "io/ioutil" "os" "path/filepath" "github.com/containerd/console" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/opts" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -46,9 +48,45 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error { var body io.Reader switch { case options.contextFromStdin(): - body = os.Stdin - remote = uploadRequestRemote + rc, isArchive, err := build.DetectArchiveReader(os.Stdin) + if err != nil { + return err + } + if isArchive { + body = rc + remote = uploadRequestRemote + } else { + dockerfileDir, err := build.WriteTempDockerfile(rc) + if err != nil { + return err + } + defer os.RemoveAll(dockerfileDir) + emptyDir, _ := ioutil.TempDir("", "stupid-empty-dir") + defer os.RemoveAll(emptyDir) + s.Allow(filesync.NewFSSyncProvider([]filesync.SyncedDir{ + { + Name: "context", + Dir: emptyDir, + }, + { + Name: "dockerfile", + Dir: string(dockerfileDir), + }, + })) + remote = clientSessionRemote + } case isLocalDir(options.context): + s.Allow(filesync.NewFSSyncProvider([]filesync.SyncedDir{ + { + Name: "context", + Dir: options.context, + Map: resetUIDAndGID, + }, + { + Name: "dockerfile", + Dir: filepath.Dir(options.dockerfileName), + }, + })) remote = clientSessionRemote case urlutil.IsGitURL(options.context): remote = options.context @@ -65,20 +103,6 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error { // statusContext = opentracing.ContextWithSpan(statusContext, span) // } - if remote == clientSessionRemote { - s.Allow(filesync.NewFSSyncProvider([]filesync.SyncedDir{ - { - Name: "context", - Dir: options.context, - Map: resetUIDAndGID, - }, - { - Name: "dockerfile", - Dir: filepath.Dir(options.dockerfileName), - }, - })) - } - s.Allow(authprovider.NewDockerAuthProvider()) eg, ctx := errgroup.WithContext(ctx)