2017-06-21 15:13:44 -04:00
|
|
|
package image
|
|
|
|
|
|
|
|
import (
|
2018-03-07 13:04:33 -05:00
|
|
|
"archive/tar"
|
2017-06-21 15:13:44 -04:00
|
|
|
"bytes"
|
2018-03-08 13:20:42 -05:00
|
|
|
"compress/gzip"
|
2018-05-03 21:02:44 -04:00
|
|
|
"context"
|
2017-06-21 15:13:44 -04:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"testing"
|
|
|
|
|
2019-01-28 08:30:31 -05:00
|
|
|
"github.com/docker/cli/cli/streams"
|
2017-08-21 16:30:09 -04:00
|
|
|
"github.com/docker/cli/internal/test"
|
2017-06-21 15:13:44 -04:00
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
"github.com/docker/docker/pkg/archive"
|
2018-03-08 13:20:42 -05:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2020-02-22 12:12:14 -05:00
|
|
|
"gotest.tools/v3/assert"
|
|
|
|
"gotest.tools/v3/fs"
|
|
|
|
"gotest.tools/v3/skip"
|
2017-06-21 15:13:44 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2018-03-08 13:20:42 -05:00
|
|
|
buffer := new(bytes.Buffer)
|
|
|
|
fakeBuild := newFakeBuild()
|
|
|
|
fakeImageBuild := func(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
|
2017-06-21 15:13:44 -04:00
|
|
|
tee := io.TeeReader(context, buffer)
|
2018-03-08 13:20:42 -05:00
|
|
|
gzipReader, err := gzip.NewReader(tee)
|
|
|
|
assert.NilError(t, err)
|
|
|
|
return fakeBuild.build(ctx, gzipReader, options)
|
2017-06-21 15:13:44 -04:00
|
|
|
}
|
|
|
|
|
2017-07-05 14:43:39 -04:00
|
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
|
2017-06-21 15:13:44 -04:00
|
|
|
dockerfile := bytes.NewBufferString(`
|
2023-02-23 09:00:59 -05:00
|
|
|
FROM alpine:frozen
|
2017-06-21 15:13:44 -04:00
|
|
|
COPY foo /
|
|
|
|
`)
|
2022-02-25 07:10:34 -05:00
|
|
|
cli.SetIn(streams.NewIn(io.NopCloser(dockerfile)))
|
2017-06-21 15:13:44 -04:00
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
dir := fs.NewDir(t, t.Name(),
|
|
|
|
fs.WithFile("foo", "some content"))
|
|
|
|
defer dir.Remove()
|
2017-06-21 15:13:44 -04:00
|
|
|
|
|
|
|
options := newBuildOptions()
|
|
|
|
options.compress = true
|
|
|
|
options.dockerfileName = "-"
|
2018-03-08 13:20:42 -05:00
|
|
|
options.context = dir.Path()
|
2018-03-06 05:15:18 -05:00
|
|
|
options.untrusted = true
|
2018-03-08 13:20:42 -05:00
|
|
|
assert.NilError(t, runBuild(cli, options))
|
2017-06-21 15:13:44 -04:00
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"}
|
|
|
|
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
|
2017-06-21 15:13:44 -04:00
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
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")
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2018-03-08 13:20:42 -05:00
|
|
|
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", `
|
2023-02-23 09:00:59 -05:00
|
|
|
FROM alpine:frozen
|
2018-03-08 13:20:42 -05:00
|
|
|
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"},
|
2017-06-21 15:13:44 -04:00
|
|
|
}
|
2022-09-29 11:21:51 -04:00
|
|
|
cmpTarHeaderNameAndOwner := cmp.Comparer(func(x, y tar.Header) bool {
|
2018-03-08 13:20:42 -05:00
|
|
|
return x.Name == y.Name && x.Uid == y.Uid && x.Gid == y.Gid
|
|
|
|
})
|
|
|
|
assert.DeepEqual(t, expected, headers, cmpTarHeaderNameAndOwner)
|
2017-06-21 15:13:44 -04:00
|
|
|
}
|
2017-07-27 05:30:18 -04:00
|
|
|
|
2018-02-17 19:40:55 -05:00
|
|
|
func TestRunBuildDockerfileOutsideContext(t *testing.T) {
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2018-02-17 19:40:55 -05:00
|
|
|
dir := fs.NewDir(t, t.Name(),
|
2018-03-08 13:20:42 -05:00
|
|
|
fs.WithFile("data", "data file"))
|
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()
|
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
fakeBuild := newFakeBuild()
|
|
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
|
2018-02-17 19:40:55 -05:00
|
|
|
|
|
|
|
options := newBuildOptions()
|
|
|
|
options.context = dir.Path()
|
|
|
|
options.dockerfileName = df.Path()
|
2018-03-08 14:56:56 -05:00
|
|
|
options.untrusted = true
|
2018-03-08 13:20:42 -05:00
|
|
|
assert.NilError(t, runBuild(cli, options))
|
2018-02-17 19:40:55 -05:00
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"}
|
|
|
|
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
|
2018-02-17 19:40:55 -05:00
|
|
|
}
|
|
|
|
|
2017-07-27 05:30:18 -04:00
|
|
|
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
|
|
|
|
// starting with `github.com/` are special-cased, and the build command attempts
|
|
|
|
// to clone the remote repo.
|
2018-03-06 15:26:53 -05:00
|
|
|
// TODO: test "context selection" logic directly when runBuild is refactored
|
|
|
|
// to support testing (ex: docker/cli#294)
|
2017-07-27 05:30:18 -04:00
|
|
|
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2020-04-07 17:24:20 -04:00
|
|
|
cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{}))
|
2018-03-06 15:26:53 -05:00
|
|
|
// Clone a small repo that exists so git doesn't prompt for credentials
|
|
|
|
cmd.SetArgs([]string{"github.com/docker/for-win"})
|
2022-02-25 07:10:34 -05:00
|
|
|
cmd.SetOut(io.Discard)
|
2017-07-27 05:30:18 -04:00
|
|
|
err := cmd.Execute()
|
2018-03-06 15:26:53 -05:00
|
|
|
assert.ErrorContains(t, err, "unable to prepare context")
|
|
|
|
assert.ErrorContains(t, err, "docker-build-git")
|
2017-07-27 05:30:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
|
|
|
|
// starting with `github.com` takes precedence over the `github.com` special
|
|
|
|
// case.
|
|
|
|
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2017-07-27 05:30:18 -04:00
|
|
|
|
2022-02-25 07:10:34 -05:00
|
|
|
buildDir := filepath.Join(t.TempDir(), "github.com", "docker", "no-such-repository")
|
2022-09-30 13:13:22 -04:00
|
|
|
err := os.MkdirAll(buildDir, 0o777)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
2022-09-30 13:13:22 -04:00
|
|
|
err = os.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0o644)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
2017-07-27 05:30:18 -04:00
|
|
|
|
|
|
|
client := test.NewFakeCli(&fakeClient{})
|
|
|
|
cmd := NewBuildCommand(client)
|
|
|
|
cmd.SetArgs([]string{buildDir})
|
2022-02-25 07:10:34 -05:00
|
|
|
cmd.SetOut(io.Discard)
|
2017-07-27 05:30:18 -04:00
|
|
|
err = cmd.Execute()
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
2017-07-27 05:30:18 -04:00
|
|
|
}
|
2018-03-07 13:04:33 -05:00
|
|
|
|
|
|
|
func TestRunBuildWithSymlinkedContext(t *testing.T) {
|
2022-09-22 10:31:28 -04:00
|
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
2018-03-07 13:04:33 -05:00
|
|
|
dockerfile := `
|
2023-02-23 09:00:59 -05:00
|
|
|
FROM alpine:frozen
|
2018-03-07 13:04:33 -05:00
|
|
|
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()
|
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
fakeBuild := newFakeBuild()
|
|
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
|
2018-03-07 13:04:33 -05:00
|
|
|
options := newBuildOptions()
|
|
|
|
options.context = tmpDir.Join("context-link")
|
2018-03-06 05:15:18 -05:00
|
|
|
options.untrusted = true
|
2018-03-07 13:04:33 -05:00
|
|
|
assert.NilError(t, runBuild(cli, options))
|
|
|
|
|
2018-03-08 13:20:42 -05:00
|
|
|
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2022-02-25 07:10:34 -05:00
|
|
|
return types.ImageBuildResponse{Body: io.NopCloser(body)}, nil
|
2018-03-08 13:20:42 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2018-03-07 13:04:33 -05:00
|
|
|
}
|