Skip to content

Commit

Permalink
build: accept -f - to read Dockerfile from stdin
Browse files Browse the repository at this point in the history
Heavily based on implementation by David Sheets

Signed-off-by: David Sheets <sheets@alum.mit.edu>
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
dsheets authored and dnephin committed Apr 5, 2017
1 parent 945a119 commit 3f6dc81
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 16 deletions.
66 changes: 60 additions & 6 deletions cli/command/image/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"time"

"github.com/docker/distribution/reference"
"github.com/docker/docker/api"
Expand All @@ -25,6 +27,7 @@ import (
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/urlutil"
runconfigopts "github.com/docker/docker/runconfig/opts"
units "github.com/docker/go-units"
Expand Down Expand Up @@ -141,6 +144,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
err error
contextDir string
tempDir string
Expand All @@ -157,6 +161,13 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
buildBuff = bytes.NewBuffer(nil)
}

if options.dockerfileName == "-" {
if specifiedContext == "-" {
return errors.New("invalid argument: can't use stdin for both build context and dockerfile")
}
dockerfileCtx = dockerCli.In()
}

switch {
case specifiedContext == "-":
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
Expand Down Expand Up @@ -214,11 +225,11 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
// removed. The daemon will remove them for us, if needed, after it
// parses the Dockerfile. Ignore errors here, as they will have been
// caught by validateContextDirectory above.
var includes = []string{"."}
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
if keepThem1 || keepThem2 {
includes = append(includes, ".dockerignore", relDockerfile)
if keep, _ := fileutils.Matches(".dockerignore", excludes); keep {
excludes = append(excludes, "!.dockerignore")
}
if keep, _ := fileutils.Matches(relDockerfile, excludes); keep && dockerfileCtx == nil {
excludes = append(excludes, "!"+relDockerfile)
}

compression := archive.Uncompressed
Expand All @@ -228,13 +239,56 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: compression,
ExcludePatterns: excludes,
IncludeFiles: includes,
})
if err != nil {
return err
}
}

// replace Dockerfile if added dynamically
if dockerfileCtx != nil {
file, err := ioutil.ReadAll(dockerfileCtx)
dockerfileCtx.Close()
if err != nil {
return err
}
now := time.Now()
hdrTmpl := &tar.Header{
Mode: 0600,
Uid: 0,
Gid: 0,
ModTime: now,
Typeflag: tar.TypeReg,
AccessTime: now,
ChangeTime: now,
}
randomName := ".dockerfile." + stringid.GenerateRandomID()[:20]

buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{
randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
return hdrTmpl, file, nil
},
".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
if h == nil {
h = hdrTmpl
}
extraIgnore := randomName + "\n"
b := &bytes.Buffer{}
if content != nil {
_, err := b.ReadFrom(content)
if err != nil {
return nil, nil, err
}
} else {
extraIgnore += ".dockerignore\n"
}
b.Write([]byte("\n" + extraIgnore))
return h, b.Bytes(), nil
},
})
relDockerfile = randomName
}

ctx := context.Background()

var resolvedTags []*resolvedTag
Expand Down
29 changes: 19 additions & 10 deletions cli/command/image/build/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
}

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 {
Expand Down Expand Up @@ -166,7 +170,7 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel
// When using a local context directory, when the Dockerfile is specified
// with the `-f/--file` option then it is considered relative to the
// current directory and not the context directory.
if dockerfileName != "" {
if dockerfileName != "" && dockerfileName != "-" {
if dockerfileName, err = filepath.Abs(dockerfileName); err != nil {
return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err)
}
Expand Down Expand Up @@ -220,6 +224,8 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi
absDockerfile = altPath
}
}
} else if absDockerfile == "-" {
absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName)
}

// If not already an absolute path, the Dockerfile path should be joined to
Expand All @@ -234,18 +240,21 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi
// an issue in golang. On Windows, EvalSymLinks does not work on UNC file
// paths (those starting with \\). This hack means that when using links
// on UNC paths, they will not be followed.
if !isUNC(absDockerfile) {
absDockerfile, err = filepath.EvalSymlinks(absDockerfile)
if err != nil {
return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err)
if givenDockerfile != "-" {
if !isUNC(absDockerfile) {
absDockerfile, err = filepath.EvalSymlinks(absDockerfile)
if err != nil {
return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err)

}
}
}

if _, err := os.Lstat(absDockerfile); err != nil {
if os.IsNotExist(err) {
return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile)
if _, err := os.Lstat(absDockerfile); err != nil {
if os.IsNotExist(err) {
return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile)
}
return "", "", errors.Errorf("unable to stat Dockerfile: %v", err)
}
return "", "", errors.Errorf("unable to stat Dockerfile: %v", err)
}

if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil {
Expand Down
75 changes: 75 additions & 0 deletions integration-cli/docker_cli_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2024,6 +2024,81 @@ func (s *DockerSuite) TestBuildNoContext(c *check.C) {
}
}

func (s *DockerSuite) TestBuildDockerfileStdin(c *check.C) {
name := "stdindockerfile"
tmpDir, err := ioutil.TempDir("", "fake-context")
c.Assert(err, check.IsNil)
err = ioutil.WriteFile(filepath.Join(tmpDir, "foo"), []byte("bar"), 0600)
c.Assert(err, check.IsNil)

icmd.RunCmd(icmd.Cmd{
Command: []string{dockerBinary, "build", "-t", name, "-f", "-", tmpDir},
Stdin: strings.NewReader(
`FROM busybox
ADD foo /foo
CMD ["cat", "/foo"]`),
}).Assert(c, icmd.Success)

res := inspectField(c, name, "Config.Cmd")
c.Assert(strings.TrimSpace(string(res)), checker.Equals, `[cat /foo]`)
}

func (s *DockerSuite) TestBuildDockerfileStdinConflict(c *check.C) {
name := "stdindockerfiletarcontext"
icmd.RunCmd(icmd.Cmd{
Command: []string{dockerBinary, "build", "-t", name, "-f", "-", "-"},
}).Assert(c, icmd.Expected{
ExitCode: 1,
Err: "use stdin for both build context and dockerfile",
})
}

func (s *DockerSuite) TestBuildDockerfileStdinNoExtraFiles(c *check.C) {
s.testBuildDockerfileStdinNoExtraFiles(c, false, false)
}

func (s *DockerSuite) TestBuildDockerfileStdinDockerignore(c *check.C) {
s.testBuildDockerfileStdinNoExtraFiles(c, true, false)
}

func (s *DockerSuite) TestBuildDockerfileStdinDockerignoreIgnored(c *check.C) {
s.testBuildDockerfileStdinNoExtraFiles(c, true, true)
}

func (s *DockerSuite) testBuildDockerfileStdinNoExtraFiles(c *check.C, hasDockerignore, ignoreDockerignore bool) {
name := "stdindockerfilenoextra"
tmpDir, err := ioutil.TempDir("", "fake-context")
c.Assert(err, check.IsNil)
err = ioutil.WriteFile(filepath.Join(tmpDir, "foo"), []byte("bar"), 0600)
c.Assert(err, check.IsNil)
if hasDockerignore {
// test that this file is removed
err = ioutil.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(""), 0600)
c.Assert(err, check.IsNil)
ignores := "Dockerfile\n"
if ignoreDockerignore {
ignores += ".dockerignore\n"
}
err = ioutil.WriteFile(filepath.Join(tmpDir, ".dockerignore"), []byte(ignores), 0600)
c.Assert(err, check.IsNil)
}

icmd.RunCmd(icmd.Cmd{
Command: []string{dockerBinary, "build", "-t", name, "-f", "-", tmpDir},
Stdin: strings.NewReader(
`FROM busybox
COPY . /baz`),
}).Assert(c, icmd.Success)

out, _ := dockerCmd(c, "run", "--rm", name, "ls", "-A", "/baz")
if hasDockerignore && !ignoreDockerignore {
c.Assert(strings.TrimSpace(string(out)), checker.Equals, ".dockerignore\nfoo")
} else {
c.Assert(strings.TrimSpace(string(out)), checker.Equals, "foo")
}

}

func (s *DockerSuite) TestBuildWithVolumeOwnership(c *check.C) {
testRequires(c, DaemonIsLinux)
name := "testbuildimg"
Expand Down
86 changes: 86 additions & 0 deletions pkg/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"syscall"

Expand Down Expand Up @@ -225,6 +226,91 @@ func CompressStream(dest io.Writer, compression Compression) (io.WriteCloser, er
}
}

// TarModifierFunc is a function that can be passed to ReplaceFileTarWrapper to
// define a modification step for a single path
type TarModifierFunc func(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error)

// ReplaceFileTarWrapper converts inputTarStream to a new tar stream
// while replacing a single file called header.Name with new contents.
// If the file with header.Name does not exist it is added to the tar stream.
// TODO: make this into a generic tar conversion function with walkFn argument
func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModifierFunc) io.ReadCloser {
pipeReader, pipeWriter := io.Pipe()

modKeys := make([]string, 0, len(mods))
for key := range mods {
modKeys = append(modKeys, key)
}
sort.Strings(modKeys)

go func() {
tarReader := tar.NewReader(inputTarStream)
tarWriter := tar.NewWriter(pipeWriter)

defer inputTarStream.Close()

loop0:
for {
hdr, err := tarReader.Next()
for len(modKeys) > 0 && (err == io.EOF || err == nil && hdr.Name >= modKeys[0]) {
var h *tar.Header
var rdr io.Reader
if hdr != nil && hdr.Name == modKeys[0] {
h = hdr
rdr = tarReader
}

h2, dt, err := mods[modKeys[0]](modKeys[0], h, rdr)
if err != nil {
pipeWriter.CloseWithError(err)
return
}
if h2 != nil {
h2.Name = modKeys[0]
h2.Size = int64(len(dt))
if err := tarWriter.WriteHeader(h2); err != nil {
pipeWriter.CloseWithError(err)
return
}
if len(dt) != 0 {
if _, err := tarWriter.Write(dt); err != nil {
pipeWriter.CloseWithError(err)
return
}
}
}
modKeys = modKeys[1:]
if h != nil {
continue loop0
}
}

if err == io.EOF {
tarWriter.Close()
pipeWriter.Close()
return
}

if err != nil {
pipeWriter.CloseWithError(err)
return
}

if err := tarWriter.WriteHeader(hdr); err != nil {
pipeWriter.CloseWithError(err)
return
}

if _, err := pools.Copy(tarWriter, tarReader); err != nil {
pipeWriter.CloseWithError(err)
return
}

}
}()
return pipeReader
}

// Extension returns the extension of a file that uses the specified compression algorithm.
func (compression *Compression) Extension() string {
switch *compression {
Expand Down
Loading

0 comments on commit 3f6dc81

Please sign in to comment.