123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723 |
- package build // import "github.com/docker/docker/integration/build"
- import (
- "archive/tar"
- "bytes"
- "context"
- "encoding/json"
- "io"
- "io/ioutil"
- "strings"
- "testing"
- "github.com/docker/docker/api/types"
- "github.com/docker/docker/api/types/filters"
- "github.com/docker/docker/api/types/versions"
- "github.com/docker/docker/errdefs"
- "github.com/docker/docker/pkg/jsonmessage"
- "github.com/docker/docker/testutil/fakecontext"
- "gotest.tools/v3/assert"
- is "gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/skip"
- )
- func TestBuildWithRemoveAndForceRemove(t *testing.T) {
- defer setupTest(t)()
- cases := []struct {
- name string
- dockerfile string
- numberOfIntermediateContainers int
- rm bool
- forceRm bool
- }{
- {
- name: "successful build with no removal",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 0`,
- numberOfIntermediateContainers: 2,
- rm: false,
- forceRm: false,
- },
- {
- name: "successful build with remove",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 0`,
- numberOfIntermediateContainers: 0,
- rm: true,
- forceRm: false,
- },
- {
- name: "successful build with remove and force remove",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 0`,
- numberOfIntermediateContainers: 0,
- rm: true,
- forceRm: true,
- },
- {
- name: "failed build with no removal",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 1`,
- numberOfIntermediateContainers: 2,
- rm: false,
- forceRm: false,
- },
- {
- name: "failed build with remove",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 1`,
- numberOfIntermediateContainers: 1,
- rm: true,
- forceRm: false,
- },
- {
- name: "failed build with remove and force remove",
- dockerfile: `FROM busybox
- RUN exit 0
- RUN exit 1`,
- numberOfIntermediateContainers: 0,
- rm: true,
- forceRm: true,
- },
- }
- client := testEnv.APIClient()
- ctx := context.Background()
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- t.Parallel()
- dockerfile := []byte(c.dockerfile)
- buff := bytes.NewBuffer(nil)
- tw := tar.NewWriter(buff)
- assert.NilError(t, tw.WriteHeader(&tar.Header{
- Name: "Dockerfile",
- Size: int64(len(dockerfile)),
- }))
- _, err := tw.Write(dockerfile)
- assert.NilError(t, err)
- assert.NilError(t, tw.Close())
- resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true})
- assert.NilError(t, err)
- defer resp.Body.Close()
- filter, err := buildContainerIdsFilter(resp.Body)
- assert.NilError(t, err)
- remainingContainers, err := client.ContainerList(ctx, types.ContainerListOptions{Filters: filter, All: true})
- assert.NilError(t, err)
- assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers))
- })
- }
- }
- func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) {
- const intermediateContainerPrefix = " ---> Running in "
- filter := filters.NewArgs()
- dec := json.NewDecoder(buildOutput)
- for {
- m := jsonmessage.JSONMessage{}
- err := dec.Decode(&m)
- if err == io.EOF {
- return filter, nil
- }
- if err != nil {
- return filter, err
- }
- if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 {
- filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):]))
- }
- }
- }
- // TestBuildMultiStageCopy verifies that copying between stages works correctly.
- //
- // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target
- // directory failed on Windows, because `os.MkdirAll()` was called with a volume
- // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}),
- // which currently isn't supported by Golang.
- func TestBuildMultiStageCopy(t *testing.T) {
- ctx := context.Background()
- dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
- assert.NilError(t, err)
- source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
- defer source.Close()
- apiclient := testEnv.APIClient()
- for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} {
- t.Run(target, func(t *testing.T) {
- imgName := strings.ToLower(t.Name())
- resp, err := apiclient.ImageBuild(
- ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Target: target,
- Tags: []string{imgName},
- },
- )
- assert.NilError(t, err)
- out := bytes.NewBuffer(nil)
- _, err = io.Copy(out, resp.Body)
- _ = resp.Body.Close()
- if err != nil {
- t.Log(out)
- }
- assert.NilError(t, err)
- // verify the image was successfully built
- _, _, err = apiclient.ImageInspectWithRaw(ctx, imgName)
- if err != nil {
- t.Log(out)
- }
- assert.NilError(t, err)
- })
- }
- }
- func TestBuildMultiStageParentConfig(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions")
- dockerfile := `
- FROM busybox AS stage0
- ENV WHO=parent
- WORKDIR /foo
- FROM stage0
- ENV WHO=sibling1
- WORKDIR sub1
- FROM stage0
- WORKDIR sub2
- `
- ctx := context.Background()
- source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
- defer source.Close()
- apiclient := testEnv.APIClient()
- imgName := strings.ToLower(t.Name())
- resp, err := apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Tags: []string{imgName},
- })
- assert.NilError(t, err)
- _, err = io.Copy(ioutil.Discard, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName)
- assert.NilError(t, err)
- expected := "/foo/sub2"
- if testEnv.DaemonInfo.OSType == "windows" {
- expected = `C:\foo\sub2`
- }
- assert.Check(t, is.Equal(expected, image.Config.WorkingDir))
- assert.Check(t, is.Contains(image.Config.Env, "WHO=parent"))
- }
- // Test cases in #36996
- func TestBuildLabelWithTargets(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38")
- skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
- imgName := strings.ToLower(t.Name() + "-a")
- testLabels := map[string]string{
- "foo": "bar",
- "dead": "beef",
- }
- dockerfile := `
- FROM busybox AS target-a
- CMD ["/dev"]
- LABEL label-a=inline-a
- FROM busybox AS target-b
- CMD ["/dist"]
- LABEL label-b=inline-b
- `
- ctx := context.Background()
- source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
- defer source.Close()
- apiclient := testEnv.APIClient()
- // For `target-a` build
- resp, err := apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Tags: []string{imgName},
- Labels: testLabels,
- Target: "target-a",
- })
- assert.NilError(t, err)
- _, err = io.Copy(ioutil.Discard, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName)
- assert.NilError(t, err)
- testLabels["label-a"] = "inline-a"
- for k, v := range testLabels {
- x, ok := image.Config.Labels[k]
- assert.Assert(t, ok)
- assert.Assert(t, x == v)
- }
- // For `target-b` build
- imgName = strings.ToLower(t.Name() + "-b")
- delete(testLabels, "label-a")
- resp, err = apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Tags: []string{imgName},
- Labels: testLabels,
- Target: "target-b",
- })
- assert.NilError(t, err)
- _, err = io.Copy(ioutil.Discard, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- image, _, err = apiclient.ImageInspectWithRaw(ctx, imgName)
- assert.NilError(t, err)
- testLabels["label-b"] = "inline-b"
- for k, v := range testLabels {
- x, ok := image.Config.Labels[k]
- assert.Assert(t, ok)
- assert.Assert(t, x == v)
- }
- }
- func TestBuildWithEmptyLayers(t *testing.T) {
- dockerfile := `
- FROM busybox
- COPY 1/ /target/
- COPY 2/ /target/
- COPY 3/ /target/
- `
- ctx := context.Background()
- source := fakecontext.New(t, "",
- fakecontext.WithDockerfile(dockerfile),
- fakecontext.WithFile("1/a", "asdf"),
- fakecontext.WithFile("2/a", "asdf"),
- fakecontext.WithFile("3/a", "asdf"))
- defer source.Close()
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- assert.NilError(t, err)
- _, err = io.Copy(ioutil.Discard, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- }
- // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to
- // multiple subsequent stages
- // #35652
- func TestBuildMultiStageOnBuild(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions")
- defer setupTest(t)()
- // test both metadata and layer based commands as they may be implemented differently
- dockerfile := `FROM busybox AS stage1
- ONBUILD RUN echo 'foo' >somefile
- ONBUILD ENV bar=baz
- FROM stage1
- # fails if ONBUILD RUN fails
- RUN cat somefile
- FROM stage1
- RUN cat somefile`
- ctx := context.Background()
- source := fakecontext.New(t, "",
- fakecontext.WithDockerfile(dockerfile))
- defer source.Close()
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out := bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- assert.Check(t, is.Contains(out.String(), "Successfully built"))
- imageIDs, err := getImageIDsFromBuild(out.Bytes())
- assert.NilError(t, err)
- assert.Assert(t, is.Equal(3, len(imageIDs)))
- image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2])
- assert.NilError(t, err)
- assert.Check(t, is.Contains(image.Config.Env, "bar=baz"))
- }
- // #35403 #36122
- func TestBuildUncleanTarFilenames(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
- skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
- ctx := context.TODO()
- defer setupTest(t)()
- dockerfile := `FROM scratch
- COPY foo /
- FROM scratch
- COPY bar /`
- buf := bytes.NewBuffer(nil)
- w := tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", dockerfile)
- writeTarRecord(t, w, "../foo", "foocontents0")
- writeTarRecord(t, w, "/bar", "barcontents0")
- err := w.Close()
- assert.NilError(t, err)
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out := bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- // repeat with changed data should not cause cache hits
- buf = bytes.NewBuffer(nil)
- w = tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", dockerfile)
- writeTarRecord(t, w, "../foo", "foocontents1")
- writeTarRecord(t, w, "/bar", "barcontents1")
- err = w.Close()
- assert.NilError(t, err)
- resp, err = apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out = bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
- }
- // docker/for-linux#135
- // #35641
- func TestBuildMultiStageLayerLeak(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
- ctx := context.TODO()
- defer setupTest(t)()
- // all commands need to match until COPY
- dockerfile := `FROM busybox
- WORKDIR /foo
- COPY foo .
- FROM busybox
- WORKDIR /foo
- COPY bar .
- RUN [ -f bar ]
- RUN [ ! -f foo ]
- `
- source := fakecontext.New(t, "",
- fakecontext.WithFile("foo", "0"),
- fakecontext.WithFile("bar", "1"),
- fakecontext.WithDockerfile(dockerfile))
- defer source.Close()
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out := bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- assert.Check(t, is.Contains(out.String(), "Successfully built"))
- }
- // #37581
- // #40444 (Windows Containers only)
- func TestBuildWithHugeFile(t *testing.T) {
- ctx := context.TODO()
- defer setupTest(t)()
- dockerfile := `FROM busybox
- `
- if testEnv.DaemonInfo.OSType == "windows" {
- dockerfile += `# create a file with size of 8GB
- RUN powershell "fsutil.exe file createnew bigfile.txt 8589934592 ; dir bigfile.txt"`
- } else {
- dockerfile += `# create a sparse file with size over 8GB
- RUN for g in $(seq 0 8); do dd if=/dev/urandom of=rnd bs=1K count=1 seek=$((1024*1024*g)) status=none; done && \
- ls -la rnd && du -sk rnd`
- }
- buf := bytes.NewBuffer(nil)
- w := tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", dockerfile)
- err := w.Close()
- assert.NilError(t, err)
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out := bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- assert.Check(t, is.Contains(out.String(), "Successfully built"))
- }
- func TestBuildWCOWSandboxSize(t *testing.T) {
- skip.If(t, testEnv.DaemonInfo.OSType != "windows", "only Windows has sandbox size control")
- ctx := context.TODO()
- defer setupTest(t)()
- dockerfile := `FROM busybox AS intermediate
- WORKDIR C:\\stuff
- # Create and delete a 21GB file
- RUN fsutil file createnew C:\\stuff\\bigfile_0.txt 22548578304 && del bigfile_0.txt
- # Create three 7GB files
- RUN fsutil file createnew C:\\stuff\\bigfile_1.txt 7516192768
- RUN fsutil file createnew C:\\stuff\\bigfile_2.txt 7516192768
- RUN fsutil file createnew C:\\stuff\\bigfile_3.txt 7516192768
- # Copy that 21GB of data out into a new target
- FROM busybox
- COPY --from=intermediate C:\\stuff C:\\stuff
- `
- buf := bytes.NewBuffer(nil)
- w := tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", dockerfile)
- err := w.Close()
- assert.NilError(t, err)
- apiclient := testEnv.APIClient()
- resp, err := apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- out := bytes.NewBuffer(nil)
- assert.NilError(t, err)
- _, err = io.Copy(out, resp.Body)
- resp.Body.Close()
- assert.NilError(t, err)
- assert.Check(t, is.Contains(out.String(), "Successfully built"))
- }
- func TestBuildWithEmptyDockerfile(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
- ctx := context.TODO()
- defer setupTest(t)()
- tests := []struct {
- name string
- dockerfile string
- expectedErr string
- }{
- {
- name: "empty-dockerfile",
- dockerfile: "",
- expectedErr: "cannot be empty",
- },
- {
- name: "empty-lines-dockerfile",
- dockerfile: `
-
-
-
- `,
- expectedErr: "file with no instructions",
- },
- {
- name: "comment-only-dockerfile",
- dockerfile: `# this is a comment`,
- expectedErr: "file with no instructions",
- },
- }
- apiclient := testEnv.APIClient()
- for _, tc := range tests {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- buf := bytes.NewBuffer(nil)
- w := tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
- err := w.Close()
- assert.NilError(t, err)
- _, err = apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- })
- assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
- })
- }
- }
- func TestBuildPreserveOwnership(t *testing.T) {
- skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
- ctx := context.Background()
- dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
- assert.NilError(t, err)
- source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
- defer source.Close()
- apiclient := testEnv.APIClient()
- for _, target := range []string{"copy_from", "copy_from_chowned"} {
- t.Run(target, func(t *testing.T) {
- resp, err := apiclient.ImageBuild(
- ctx,
- source.AsTarReader(t),
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Target: target,
- },
- )
- assert.NilError(t, err)
- out := bytes.NewBuffer(nil)
- _, err = io.Copy(out, resp.Body)
- _ = resp.Body.Close()
- if err != nil {
- t.Log(out)
- }
- assert.NilError(t, err)
- })
- }
- }
- func TestBuildPlatformInvalid(t *testing.T) {
- skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
- ctx := context.Background()
- defer setupTest(t)()
- dockerfile := `FROM busybox
- `
- buf := bytes.NewBuffer(nil)
- w := tar.NewWriter(buf)
- writeTarRecord(t, w, "Dockerfile", dockerfile)
- err := w.Close()
- assert.NilError(t, err)
- apiclient := testEnv.APIClient()
- _, err = apiclient.ImageBuild(ctx,
- buf,
- types.ImageBuildOptions{
- Remove: true,
- ForceRemove: true,
- Platform: "foobar",
- })
- assert.Assert(t, err != nil)
- assert.ErrorContains(t, err, "unknown operating system or architecture")
- assert.Assert(t, errdefs.IsInvalidParameter(err))
- }
- func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
- err := w.WriteHeader(&tar.Header{
- Name: fn,
- Mode: 0600,
- Size: int64(len(contents)),
- Typeflag: '0',
- })
- assert.NilError(t, err)
- _, err = w.Write([]byte(contents))
- assert.NilError(t, err)
- }
- type buildLine struct {
- Stream string
- Aux struct {
- ID string
- }
- }
- func getImageIDsFromBuild(output []byte) ([]string, error) {
- var ids []string
- for _, line := range bytes.Split(output, []byte("\n")) {
- if len(line) == 0 {
- continue
- }
- entry := buildLine{}
- if err := json.Unmarshal(line, &entry); err != nil {
- return nil, err
- }
- if entry.Aux.ID != "" {
- ids = append(ids, entry.Aux.ID)
- }
- }
- return ids, nil
- }
|