DockerCLI/cli/command/image/build_test.go

279 lines
7.7 KiB
Go
Raw Normal View History

package image
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"testing"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/google/go-cmp/cmp"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
"gotest.tools/v3/skip"
)
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
buffer := new(bytes.Buffer)
fakeBuild := newFakeBuild()
fakeImageBuild := func(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
tee := io.TeeReader(context, buffer)
gzipReader, err := gzip.NewReader(tee)
assert.NilError(t, err)
return fakeBuild.build(ctx, gzipReader, options)
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
dockerfile := bytes.NewBufferString(`
FROM alpine:3.6
COPY foo /
`)
cli.SetIn(streams.NewIn(ioutil.NopCloser(dockerfile)))
dir := fs.NewDir(t, t.Name(),
fs.WithFile("foo", "some content"))
defer dir.Remove()
options := newBuildOptions()
options.compress = true
options.dockerfileName = "-"
options.context = dir.Path()
options.untrusted = true
assert.NilError(t, runBuild(cli, options))
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"}
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
header := buffer.Bytes()[:10]
assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
}
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
skip.If(t, os.Getuid() != 0, "root is required to chown files")
fakeBuild := newFakeBuild()
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
dir := fs.NewDir(t, "test-build-context",
fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
fs.WithFile("Dockerfile", `
FROM alpine:3.6
COPY foo bar /
`),
)
defer dir.Remove()
options := newBuildOptions()
options.context = dir.Path()
options.untrusted = true
assert.NilError(t, runBuild(cli, options))
headers := fakeBuild.headers(t)
expected := []*tar.Header{
{Name: "Dockerfile"},
{Name: "foo"},
}
var cmpTarHeaderNameAndOwner = cmp.Comparer(func(x, y tar.Header) bool {
return x.Name == y.Name && x.Uid == y.Uid && x.Gid == y.Gid
})
assert.DeepEqual(t, expected, headers, cmpTarHeaderNameAndOwner)
}
Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-02-17 19:40:55 -05:00
func TestRunBuildDockerfileOutsideContext(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithFile("data", "data file"))
Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-02-17 19:40:55 -05:00
defer dir.Remove()
// Dockerfile outside of build-context
df := fs.NewFile(t, t.Name(),
fs.WithContent(`
FROM FOOBAR
COPY data /data
`),
)
defer df.Remove()
fakeBuild := newFakeBuild()
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-02-17 19:40:55 -05:00
options := newBuildOptions()
options.context = dir.Path()
options.dockerfileName = df.Path()
options.untrusted = true
assert.NilError(t, runBuild(cli, options))
Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-02-17 19:40:55 -05:00
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"}
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-02-17 19:40:55 -05:00
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
// starting with `github.com/` are special-cased, and the build command attempts
// to clone the remote repo.
// TODO: test "context selection" logic directly when runBuild is refactored
// to support testing (ex: docker/cli#294)
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
cmd := NewBuildCommand(test.NewFakeCli(nil))
// Clone a small repo that exists so git doesn't prompt for credentials
cmd.SetArgs([]string{"github.com/docker/for-win"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to prepare context")
assert.ErrorContains(t, err, "docker-build-git")
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
// starting with `github.com` takes precedence over the `github.com` special
// case.
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-build-from-local-dir-")
assert.NilError(t, err)
defer os.RemoveAll(tmpDir)
buildDir := filepath.Join(tmpDir, "github.com", "docker", "no-such-repository")
err = os.MkdirAll(buildDir, 0777)
assert.NilError(t, err)
err = ioutil.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0644)
assert.NilError(t, err)
client := test.NewFakeCli(&fakeClient{})
cmd := NewBuildCommand(client)
cmd.SetArgs([]string{buildDir})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
assert.NilError(t, err)
}
func TestRunBuildWithSymlinkedContext(t *testing.T) {
dockerfile := `
FROM alpine:3.6
RUN echo hello world
`
tmpDir := fs.NewDir(t, t.Name(),
fs.WithDir("context",
fs.WithFile("Dockerfile", dockerfile)),
fs.WithSymlink("context-link", "context"))
defer tmpDir.Remove()
fakeBuild := newFakeBuild()
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
options := newBuildOptions()
options.context = tmpDir.Join("context-link")
options.untrusted = true
assert.NilError(t, runBuild(cli, options))
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
}
func TestParseSecret(t *testing.T) {
type testcase struct {
value string
errExpected bool
errMatch string
filesource *secretsprovider.FileSource
}
var testcases = []testcase{
{
value: "",
errExpected: true,
}, {
value: "foobar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo,bar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo=bar",
errExpected: true,
errMatch: "unexpected key",
}, {
value: "src=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "source=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "id=mysecret",
filesource: &secretsprovider.FileSource{ID: "mysecret"},
}, {
value: "id=mysecret,src=somefile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,source=somefile,type=file",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,src=somefile,src=othersecretfile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "othersecretfile"},
}, {
value: "type=invalid",
errExpected: true,
errMatch: "unsupported secret type",
},
}
for _, tc := range testcases {
t.Run(tc.value, func(t *testing.T) {
secret, err := parseSecret(tc.value)
assert.Equal(t, err != nil, tc.errExpected, fmt.Sprintf("err=%v errExpected=%t", err, tc.errExpected))
if tc.errMatch != "" {
assert.ErrorContains(t, err, tc.errMatch)
}
assert.DeepEqual(t, secret, tc.filesource)
})
}
}
type fakeBuild struct {
context *tar.Reader
options types.ImageBuildOptions
}
func newFakeBuild() *fakeBuild {
return &fakeBuild{}
}
func (f *fakeBuild) build(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
f.context = tar.NewReader(context)
f.options = options
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
t.Helper()
headers := []*tar.Header{}
for {
hdr, err := f.context.Next()
switch err {
case io.EOF:
return headers
case nil:
headers = append(headers, hdr)
default:
assert.NilError(t, err)
}
}
}
func (f *fakeBuild) filenames(t *testing.T) []string {
t.Helper()
names := []string{}
for _, header := range f.headers(t) {
names = append(names, header.Name)
}
sort.Strings(names)
return names
}