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/integration-cli/cli/build/fakecontext" "github.com/docker/docker/integration/internal/request" "github.com/docker/docker/pkg/jsonmessage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuildWithRemoveAndForceRemove(t *testing.T) { defer setupTest(t)() t.Parallel() 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 := request.NewAPIClient(t) 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) require.NoError(t, tw.WriteHeader(&tar.Header{ Name: "Dockerfile", Size: int64(len(dockerfile)), })) _, err := tw.Write(dockerfile) require.NoError(t, err) require.NoError(t, tw.Close()) resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true}) require.NoError(t, err) defer resp.Body.Close() filter, err := buildContainerIdsFilter(resp.Body) require.NoError(t, err) remainingContainers, err := client.ContainerList(ctx, types.ContainerListOptions{Filters: filter, All: true}) require.NoError(t, err) require.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):])) } } } func TestBuildMultiStageParentConfig(t *testing.T) { 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() resp, err := apiclient.ImageBuild(ctx, source.AsTarReader(t), types.ImageBuildOptions{ Remove: true, ForceRemove: true, Tags: []string{"build1"}, }) require.NoError(t, err) _, err = io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() require.NoError(t, err) image, _, err := apiclient.ImageInspectWithRaw(ctx, "build1") require.NoError(t, err) assert.Equal(t, "/foo/sub2", image.Config.WorkingDir) assert.Contains(t, image.Config.Env, "WHO=parent") } 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, }) require.NoError(t, err) _, err = io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() require.NoError(t, err) } // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to // multiple subsequent stages // #35652 func TestBuildMultiStageOnBuild(t *testing.T) { 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 RUN cat somefile # fails if ONBUILD RUN fails 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) require.NoError(t, err) _, err = io.Copy(out, resp.Body) resp.Body.Close() require.NoError(t, err) assert.Contains(t, out.String(), "Successfully built") imageIDs, err := getImageIDsFromBuild(out.Bytes()) require.NoError(t, err) assert.Equal(t, 3, len(imageIDs)) image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2]) require.NoError(t, err) assert.Contains(t, image.Config.Env, "bar=baz") } // #35403 #36122 func TestBuildUncleanTarFilenames(t *testing.T) { 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() require.NoError(t, err) apiclient := testEnv.APIClient() resp, err := apiclient.ImageBuild(ctx, buf, types.ImageBuildOptions{ Remove: true, ForceRemove: true, }) out := bytes.NewBuffer(nil) require.NoError(t, err) _, err = io.Copy(out, resp.Body) resp.Body.Close() require.NoError(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() require.NoError(t, err) resp, err = apiclient.ImageBuild(ctx, buf, types.ImageBuildOptions{ Remove: true, ForceRemove: true, }) out = bytes.NewBuffer(nil) require.NoError(t, err) _, err = io.Copy(out, resp.Body) resp.Body.Close() require.NoError(t, err) require.NotContains(t, out.String(), "Using cache") } // docker/for-linux#135 // #35641 func TestBuildMultiStageLayerLeak(t *testing.T) { 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) require.NoError(t, err) _, err = io.Copy(out, resp.Body) resp.Body.Close() require.NoError(t, err) assert.Contains(t, out.String(), "Successfully built") } 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', }) require.NoError(t, err) _, err = w.Write([]byte(contents)) require.NoError(t, err) } type buildLine struct { Stream string Aux struct { ID string } } func getImageIDsFromBuild(output []byte) ([]string, error) { 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 }