github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/integration/build/build_test.go (about)

     1  package build // import "github.com/docker/docker/integration/build"
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"io"
     9  	"io/ioutil"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/api/types/filters"
    15  	"github.com/docker/docker/api/types/versions"
    16  	"github.com/docker/docker/errdefs"
    17  	"github.com/docker/docker/pkg/jsonmessage"
    18  	"github.com/docker/docker/testutil/fakecontext"
    19  	"gotest.tools/v3/assert"
    20  	is "gotest.tools/v3/assert/cmp"
    21  	"gotest.tools/v3/skip"
    22  )
    23  
    24  func TestBuildWithRemoveAndForceRemove(t *testing.T) {
    25  	defer setupTest(t)()
    26  
    27  	cases := []struct {
    28  		name                           string
    29  		dockerfile                     string
    30  		numberOfIntermediateContainers int
    31  		rm                             bool
    32  		forceRm                        bool
    33  	}{
    34  		{
    35  			name: "successful build with no removal",
    36  			dockerfile: `FROM busybox
    37  			RUN exit 0
    38  			RUN exit 0`,
    39  			numberOfIntermediateContainers: 2,
    40  			rm:                             false,
    41  			forceRm:                        false,
    42  		},
    43  		{
    44  			name: "successful build with remove",
    45  			dockerfile: `FROM busybox
    46  			RUN exit 0
    47  			RUN exit 0`,
    48  			numberOfIntermediateContainers: 0,
    49  			rm:                             true,
    50  			forceRm:                        false,
    51  		},
    52  		{
    53  			name: "successful build with remove and force remove",
    54  			dockerfile: `FROM busybox
    55  			RUN exit 0
    56  			RUN exit 0`,
    57  			numberOfIntermediateContainers: 0,
    58  			rm:                             true,
    59  			forceRm:                        true,
    60  		},
    61  		{
    62  			name: "failed build with no removal",
    63  			dockerfile: `FROM busybox
    64  			RUN exit 0
    65  			RUN exit 1`,
    66  			numberOfIntermediateContainers: 2,
    67  			rm:                             false,
    68  			forceRm:                        false,
    69  		},
    70  		{
    71  			name: "failed build with remove",
    72  			dockerfile: `FROM busybox
    73  			RUN exit 0
    74  			RUN exit 1`,
    75  			numberOfIntermediateContainers: 1,
    76  			rm:                             true,
    77  			forceRm:                        false,
    78  		},
    79  		{
    80  			name: "failed build with remove and force remove",
    81  			dockerfile: `FROM busybox
    82  			RUN exit 0
    83  			RUN exit 1`,
    84  			numberOfIntermediateContainers: 0,
    85  			rm:                             true,
    86  			forceRm:                        true,
    87  		},
    88  	}
    89  
    90  	client := testEnv.APIClient()
    91  	ctx := context.Background()
    92  	for _, c := range cases {
    93  		t.Run(c.name, func(t *testing.T) {
    94  			t.Parallel()
    95  			dockerfile := []byte(c.dockerfile)
    96  
    97  			buff := bytes.NewBuffer(nil)
    98  			tw := tar.NewWriter(buff)
    99  			assert.NilError(t, tw.WriteHeader(&tar.Header{
   100  				Name: "Dockerfile",
   101  				Size: int64(len(dockerfile)),
   102  			}))
   103  			_, err := tw.Write(dockerfile)
   104  			assert.NilError(t, err)
   105  			assert.NilError(t, tw.Close())
   106  			resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true})
   107  			assert.NilError(t, err)
   108  			defer resp.Body.Close()
   109  			filter, err := buildContainerIdsFilter(resp.Body)
   110  			assert.NilError(t, err)
   111  			remainingContainers, err := client.ContainerList(ctx, types.ContainerListOptions{Filters: filter, All: true})
   112  			assert.NilError(t, err)
   113  			assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers))
   114  		})
   115  	}
   116  }
   117  
   118  func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) {
   119  	const intermediateContainerPrefix = " ---> Running in "
   120  	filter := filters.NewArgs()
   121  
   122  	dec := json.NewDecoder(buildOutput)
   123  	for {
   124  		m := jsonmessage.JSONMessage{}
   125  		err := dec.Decode(&m)
   126  		if err == io.EOF {
   127  			return filter, nil
   128  		}
   129  		if err != nil {
   130  			return filter, err
   131  		}
   132  		if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 {
   133  			filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):]))
   134  		}
   135  	}
   136  }
   137  
   138  // TestBuildMultiStageCopy verifies that copying between stages works correctly.
   139  //
   140  // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target
   141  // directory failed on Windows, because `os.MkdirAll()` was called with a volume
   142  // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}),
   143  // which currently isn't supported by Golang.
   144  func TestBuildMultiStageCopy(t *testing.T) {
   145  	ctx := context.Background()
   146  
   147  	dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
   148  	assert.NilError(t, err)
   149  
   150  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
   151  	defer source.Close()
   152  
   153  	apiclient := testEnv.APIClient()
   154  
   155  	for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} {
   156  		t.Run(target, func(t *testing.T) {
   157  			imgName := strings.ToLower(t.Name())
   158  
   159  			resp, err := apiclient.ImageBuild(
   160  				ctx,
   161  				source.AsTarReader(t),
   162  				types.ImageBuildOptions{
   163  					Remove:      true,
   164  					ForceRemove: true,
   165  					Target:      target,
   166  					Tags:        []string{imgName},
   167  				},
   168  			)
   169  			assert.NilError(t, err)
   170  
   171  			out := bytes.NewBuffer(nil)
   172  			_, err = io.Copy(out, resp.Body)
   173  			_ = resp.Body.Close()
   174  			if err != nil {
   175  				t.Log(out)
   176  			}
   177  			assert.NilError(t, err)
   178  
   179  			// verify the image was successfully built
   180  			_, _, err = apiclient.ImageInspectWithRaw(ctx, imgName)
   181  			if err != nil {
   182  				t.Log(out)
   183  			}
   184  			assert.NilError(t, err)
   185  		})
   186  	}
   187  }
   188  
   189  func TestBuildMultiStageParentConfig(t *testing.T) {
   190  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions")
   191  	dockerfile := `
   192  		FROM busybox AS stage0
   193  		ENV WHO=parent
   194  		WORKDIR /foo
   195  
   196  		FROM stage0
   197  		ENV WHO=sibling1
   198  		WORKDIR sub1
   199  
   200  		FROM stage0
   201  		WORKDIR sub2
   202  	`
   203  	ctx := context.Background()
   204  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
   205  	defer source.Close()
   206  
   207  	apiclient := testEnv.APIClient()
   208  	imgName := strings.ToLower(t.Name())
   209  	resp, err := apiclient.ImageBuild(ctx,
   210  		source.AsTarReader(t),
   211  		types.ImageBuildOptions{
   212  			Remove:      true,
   213  			ForceRemove: true,
   214  			Tags:        []string{imgName},
   215  		})
   216  	assert.NilError(t, err)
   217  	_, err = io.Copy(ioutil.Discard, resp.Body)
   218  	resp.Body.Close()
   219  	assert.NilError(t, err)
   220  
   221  	image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName)
   222  	assert.NilError(t, err)
   223  
   224  	expected := "/foo/sub2"
   225  	if testEnv.DaemonInfo.OSType == "windows" {
   226  		expected = `C:\foo\sub2`
   227  	}
   228  	assert.Check(t, is.Equal(expected, image.Config.WorkingDir))
   229  	assert.Check(t, is.Contains(image.Config.Env, "WHO=parent"))
   230  }
   231  
   232  // Test cases in #36996
   233  func TestBuildLabelWithTargets(t *testing.T) {
   234  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38")
   235  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   236  	imgName := strings.ToLower(t.Name() + "-a")
   237  	testLabels := map[string]string{
   238  		"foo":  "bar",
   239  		"dead": "beef",
   240  	}
   241  
   242  	dockerfile := `
   243  		FROM busybox AS target-a
   244  		CMD ["/dev"]
   245  		LABEL label-a=inline-a
   246  		FROM busybox AS target-b
   247  		CMD ["/dist"]
   248  		LABEL label-b=inline-b
   249  		`
   250  
   251  	ctx := context.Background()
   252  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
   253  	defer source.Close()
   254  
   255  	apiclient := testEnv.APIClient()
   256  	// For `target-a` build
   257  	resp, err := apiclient.ImageBuild(ctx,
   258  		source.AsTarReader(t),
   259  		types.ImageBuildOptions{
   260  			Remove:      true,
   261  			ForceRemove: true,
   262  			Tags:        []string{imgName},
   263  			Labels:      testLabels,
   264  			Target:      "target-a",
   265  		})
   266  	assert.NilError(t, err)
   267  	_, err = io.Copy(ioutil.Discard, resp.Body)
   268  	resp.Body.Close()
   269  	assert.NilError(t, err)
   270  
   271  	image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName)
   272  	assert.NilError(t, err)
   273  
   274  	testLabels["label-a"] = "inline-a"
   275  	for k, v := range testLabels {
   276  		x, ok := image.Config.Labels[k]
   277  		assert.Assert(t, ok)
   278  		assert.Assert(t, x == v)
   279  	}
   280  
   281  	// For `target-b` build
   282  	imgName = strings.ToLower(t.Name() + "-b")
   283  	delete(testLabels, "label-a")
   284  	resp, err = apiclient.ImageBuild(ctx,
   285  		source.AsTarReader(t),
   286  		types.ImageBuildOptions{
   287  			Remove:      true,
   288  			ForceRemove: true,
   289  			Tags:        []string{imgName},
   290  			Labels:      testLabels,
   291  			Target:      "target-b",
   292  		})
   293  	assert.NilError(t, err)
   294  	_, err = io.Copy(ioutil.Discard, resp.Body)
   295  	resp.Body.Close()
   296  	assert.NilError(t, err)
   297  
   298  	image, _, err = apiclient.ImageInspectWithRaw(ctx, imgName)
   299  	assert.NilError(t, err)
   300  
   301  	testLabels["label-b"] = "inline-b"
   302  	for k, v := range testLabels {
   303  		x, ok := image.Config.Labels[k]
   304  		assert.Assert(t, ok)
   305  		assert.Assert(t, x == v)
   306  	}
   307  }
   308  
   309  func TestBuildWithEmptyLayers(t *testing.T) {
   310  	dockerfile := `
   311  		FROM    busybox
   312  		COPY    1/ /target/
   313  		COPY    2/ /target/
   314  		COPY    3/ /target/
   315  	`
   316  	ctx := context.Background()
   317  	source := fakecontext.New(t, "",
   318  		fakecontext.WithDockerfile(dockerfile),
   319  		fakecontext.WithFile("1/a", "asdf"),
   320  		fakecontext.WithFile("2/a", "asdf"),
   321  		fakecontext.WithFile("3/a", "asdf"))
   322  	defer source.Close()
   323  
   324  	apiclient := testEnv.APIClient()
   325  	resp, err := apiclient.ImageBuild(ctx,
   326  		source.AsTarReader(t),
   327  		types.ImageBuildOptions{
   328  			Remove:      true,
   329  			ForceRemove: true,
   330  		})
   331  	assert.NilError(t, err)
   332  	_, err = io.Copy(ioutil.Discard, resp.Body)
   333  	resp.Body.Close()
   334  	assert.NilError(t, err)
   335  }
   336  
   337  // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to
   338  // multiple subsequent stages
   339  // #35652
   340  func TestBuildMultiStageOnBuild(t *testing.T) {
   341  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions")
   342  	defer setupTest(t)()
   343  	// test both metadata and layer based commands as they may be implemented differently
   344  	dockerfile := `FROM busybox AS stage1
   345  ONBUILD RUN echo 'foo' >somefile
   346  ONBUILD ENV bar=baz
   347  
   348  FROM stage1
   349  # fails if ONBUILD RUN fails
   350  RUN cat somefile
   351  
   352  FROM stage1
   353  RUN cat somefile`
   354  
   355  	ctx := context.Background()
   356  	source := fakecontext.New(t, "",
   357  		fakecontext.WithDockerfile(dockerfile))
   358  	defer source.Close()
   359  
   360  	apiclient := testEnv.APIClient()
   361  	resp, err := apiclient.ImageBuild(ctx,
   362  		source.AsTarReader(t),
   363  		types.ImageBuildOptions{
   364  			Remove:      true,
   365  			ForceRemove: true,
   366  		})
   367  
   368  	out := bytes.NewBuffer(nil)
   369  	assert.NilError(t, err)
   370  	_, err = io.Copy(out, resp.Body)
   371  	resp.Body.Close()
   372  	assert.NilError(t, err)
   373  
   374  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   375  
   376  	imageIDs, err := getImageIDsFromBuild(out.Bytes())
   377  	assert.NilError(t, err)
   378  	assert.Assert(t, is.Equal(3, len(imageIDs)))
   379  
   380  	image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2])
   381  	assert.NilError(t, err)
   382  	assert.Check(t, is.Contains(image.Config.Env, "bar=baz"))
   383  }
   384  
   385  // #35403 #36122
   386  func TestBuildUncleanTarFilenames(t *testing.T) {
   387  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
   388  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   389  
   390  	ctx := context.TODO()
   391  	defer setupTest(t)()
   392  
   393  	dockerfile := `FROM scratch
   394  COPY foo /
   395  FROM scratch
   396  COPY bar /`
   397  
   398  	buf := bytes.NewBuffer(nil)
   399  	w := tar.NewWriter(buf)
   400  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   401  	writeTarRecord(t, w, "../foo", "foocontents0")
   402  	writeTarRecord(t, w, "/bar", "barcontents0")
   403  	err := w.Close()
   404  	assert.NilError(t, err)
   405  
   406  	apiclient := testEnv.APIClient()
   407  	resp, err := apiclient.ImageBuild(ctx,
   408  		buf,
   409  		types.ImageBuildOptions{
   410  			Remove:      true,
   411  			ForceRemove: true,
   412  		})
   413  
   414  	out := bytes.NewBuffer(nil)
   415  	assert.NilError(t, err)
   416  	_, err = io.Copy(out, resp.Body)
   417  	resp.Body.Close()
   418  	assert.NilError(t, err)
   419  
   420  	// repeat with changed data should not cause cache hits
   421  
   422  	buf = bytes.NewBuffer(nil)
   423  	w = tar.NewWriter(buf)
   424  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   425  	writeTarRecord(t, w, "../foo", "foocontents1")
   426  	writeTarRecord(t, w, "/bar", "barcontents1")
   427  	err = w.Close()
   428  	assert.NilError(t, err)
   429  
   430  	resp, err = apiclient.ImageBuild(ctx,
   431  		buf,
   432  		types.ImageBuildOptions{
   433  			Remove:      true,
   434  			ForceRemove: true,
   435  		})
   436  
   437  	out = bytes.NewBuffer(nil)
   438  	assert.NilError(t, err)
   439  	_, err = io.Copy(out, resp.Body)
   440  	resp.Body.Close()
   441  	assert.NilError(t, err)
   442  	assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
   443  }
   444  
   445  // docker/for-linux#135
   446  // #35641
   447  func TestBuildMultiStageLayerLeak(t *testing.T) {
   448  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
   449  	ctx := context.TODO()
   450  	defer setupTest(t)()
   451  
   452  	// all commands need to match until COPY
   453  	dockerfile := `FROM busybox
   454  WORKDIR /foo
   455  COPY foo .
   456  FROM busybox
   457  WORKDIR /foo
   458  COPY bar .
   459  RUN [ -f bar ]
   460  RUN [ ! -f foo ]
   461  `
   462  
   463  	source := fakecontext.New(t, "",
   464  		fakecontext.WithFile("foo", "0"),
   465  		fakecontext.WithFile("bar", "1"),
   466  		fakecontext.WithDockerfile(dockerfile))
   467  	defer source.Close()
   468  
   469  	apiclient := testEnv.APIClient()
   470  	resp, err := apiclient.ImageBuild(ctx,
   471  		source.AsTarReader(t),
   472  		types.ImageBuildOptions{
   473  			Remove:      true,
   474  			ForceRemove: true,
   475  		})
   476  
   477  	out := bytes.NewBuffer(nil)
   478  	assert.NilError(t, err)
   479  	_, err = io.Copy(out, resp.Body)
   480  	resp.Body.Close()
   481  	assert.NilError(t, err)
   482  
   483  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   484  }
   485  
   486  // #37581
   487  // #40444 (Windows Containers only)
   488  func TestBuildWithHugeFile(t *testing.T) {
   489  	ctx := context.TODO()
   490  	defer setupTest(t)()
   491  
   492  	dockerfile := `FROM busybox
   493  `
   494  
   495  	if testEnv.DaemonInfo.OSType == "windows" {
   496  		dockerfile += `# create a file with size of 8GB
   497  RUN powershell "fsutil.exe file createnew bigfile.txt 8589934592 ; dir bigfile.txt"`
   498  	} else {
   499  		dockerfile += `# create a sparse file with size over 8GB
   500  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 && \
   501      ls -la rnd && du -sk rnd`
   502  	}
   503  
   504  	buf := bytes.NewBuffer(nil)
   505  	w := tar.NewWriter(buf)
   506  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   507  	err := w.Close()
   508  	assert.NilError(t, err)
   509  
   510  	apiclient := testEnv.APIClient()
   511  	resp, err := apiclient.ImageBuild(ctx,
   512  		buf,
   513  		types.ImageBuildOptions{
   514  			Remove:      true,
   515  			ForceRemove: true,
   516  		})
   517  
   518  	out := bytes.NewBuffer(nil)
   519  	assert.NilError(t, err)
   520  	_, err = io.Copy(out, resp.Body)
   521  	resp.Body.Close()
   522  	assert.NilError(t, err)
   523  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   524  }
   525  
   526  func TestBuildWCOWSandboxSize(t *testing.T) {
   527  	skip.If(t, testEnv.DaemonInfo.OSType != "windows", "only Windows has sandbox size control")
   528  	ctx := context.TODO()
   529  	defer setupTest(t)()
   530  
   531  	dockerfile := `FROM busybox AS intermediate
   532  WORKDIR C:\\stuff
   533  # Create and delete a 21GB file
   534  RUN fsutil file createnew C:\\stuff\\bigfile_0.txt 22548578304 && del bigfile_0.txt
   535  # Create three 7GB files
   536  RUN fsutil file createnew C:\\stuff\\bigfile_1.txt 7516192768
   537  RUN fsutil file createnew C:\\stuff\\bigfile_2.txt 7516192768
   538  RUN fsutil file createnew C:\\stuff\\bigfile_3.txt 7516192768
   539  # Copy that 21GB of data out into a new target
   540  FROM busybox
   541  COPY --from=intermediate C:\\stuff C:\\stuff
   542  `
   543  
   544  	buf := bytes.NewBuffer(nil)
   545  	w := tar.NewWriter(buf)
   546  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   547  	err := w.Close()
   548  	assert.NilError(t, err)
   549  
   550  	apiclient := testEnv.APIClient()
   551  	resp, err := apiclient.ImageBuild(ctx,
   552  		buf,
   553  		types.ImageBuildOptions{
   554  			Remove:      true,
   555  			ForceRemove: true,
   556  		})
   557  
   558  	out := bytes.NewBuffer(nil)
   559  	assert.NilError(t, err)
   560  	_, err = io.Copy(out, resp.Body)
   561  	resp.Body.Close()
   562  	assert.NilError(t, err)
   563  	// The test passes if either:
   564  	// - the image build succeeded; or
   565  	// - The "COPY --from=intermediate" step ran out of space during re-exec'd writing of the transport layer information to hcsshim's temp directory
   566  	// The latter case means we finished the COPY operation, so the sandbox must have been larger than 20GB, which was the test,
   567  	// and _then_ ran out of space on the host during `importLayer` in the WindowsFilter graph driver, while committing the layer.
   568  	// See https://github.com/moby/moby/pull/41636#issuecomment-723038517 for more details on the operations being done here.
   569  	// Specifically, this happens on the Docker Jenkins CI Windows-RS5 build nodes.
   570  	// The two parts of the acceptable-failure case are on different lines, so we need two regexp checks.
   571  	assert.Check(t, is.Regexp("Successfully built|COPY --from=intermediate", out.String()))
   572  	assert.Check(t, is.Regexp("Successfully built|re-exec error: exit status 1: output: write.*daemon\\\\\\\\tmp\\\\\\\\hcs.*bigfile_[1-3].txt: There is not enough space on the disk.", out.String()))
   573  }
   574  
   575  func TestBuildWithEmptyDockerfile(t *testing.T) {
   576  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   577  	ctx := context.TODO()
   578  	defer setupTest(t)()
   579  
   580  	tests := []struct {
   581  		name        string
   582  		dockerfile  string
   583  		expectedErr string
   584  	}{
   585  		{
   586  			name:        "empty-dockerfile",
   587  			dockerfile:  "",
   588  			expectedErr: "cannot be empty",
   589  		},
   590  		{
   591  			name: "empty-lines-dockerfile",
   592  			dockerfile: `
   593  			
   594  			
   595  			
   596  			`,
   597  			expectedErr: "file with no instructions",
   598  		},
   599  		{
   600  			name:        "comment-only-dockerfile",
   601  			dockerfile:  `# this is a comment`,
   602  			expectedErr: "file with no instructions",
   603  		},
   604  	}
   605  
   606  	apiclient := testEnv.APIClient()
   607  
   608  	for _, tc := range tests {
   609  		tc := tc
   610  		t.Run(tc.name, func(t *testing.T) {
   611  			t.Parallel()
   612  
   613  			buf := bytes.NewBuffer(nil)
   614  			w := tar.NewWriter(buf)
   615  			writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
   616  			err := w.Close()
   617  			assert.NilError(t, err)
   618  
   619  			_, err = apiclient.ImageBuild(ctx,
   620  				buf,
   621  				types.ImageBuildOptions{
   622  					Remove:      true,
   623  					ForceRemove: true,
   624  				})
   625  
   626  			assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
   627  		})
   628  	}
   629  }
   630  
   631  func TestBuildPreserveOwnership(t *testing.T) {
   632  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   633  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   634  
   635  	ctx := context.Background()
   636  
   637  	dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
   638  	assert.NilError(t, err)
   639  
   640  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
   641  	defer source.Close()
   642  
   643  	apiclient := testEnv.APIClient()
   644  
   645  	for _, target := range []string{"copy_from", "copy_from_chowned"} {
   646  		t.Run(target, func(t *testing.T) {
   647  			resp, err := apiclient.ImageBuild(
   648  				ctx,
   649  				source.AsTarReader(t),
   650  				types.ImageBuildOptions{
   651  					Remove:      true,
   652  					ForceRemove: true,
   653  					Target:      target,
   654  				},
   655  			)
   656  			assert.NilError(t, err)
   657  
   658  			out := bytes.NewBuffer(nil)
   659  			_, err = io.Copy(out, resp.Body)
   660  			_ = resp.Body.Close()
   661  			if err != nil {
   662  				t.Log(out)
   663  			}
   664  			assert.NilError(t, err)
   665  		})
   666  	}
   667  }
   668  
   669  func TestBuildPlatformInvalid(t *testing.T) {
   670  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
   671  
   672  	ctx := context.Background()
   673  	defer setupTest(t)()
   674  
   675  	dockerfile := `FROM busybox
   676  `
   677  
   678  	buf := bytes.NewBuffer(nil)
   679  	w := tar.NewWriter(buf)
   680  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   681  	err := w.Close()
   682  	assert.NilError(t, err)
   683  
   684  	apiclient := testEnv.APIClient()
   685  	_, err = apiclient.ImageBuild(ctx,
   686  		buf,
   687  		types.ImageBuildOptions{
   688  			Remove:      true,
   689  			ForceRemove: true,
   690  			Platform:    "foobar",
   691  		})
   692  
   693  	assert.Assert(t, err != nil)
   694  	assert.ErrorContains(t, err, "unknown operating system or architecture")
   695  	assert.Assert(t, errdefs.IsInvalidParameter(err))
   696  }
   697  
   698  func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
   699  	err := w.WriteHeader(&tar.Header{
   700  		Name:     fn,
   701  		Mode:     0600,
   702  		Size:     int64(len(contents)),
   703  		Typeflag: '0',
   704  	})
   705  	assert.NilError(t, err)
   706  	_, err = w.Write([]byte(contents))
   707  	assert.NilError(t, err)
   708  }
   709  
   710  type buildLine struct {
   711  	Stream string
   712  	Aux    struct {
   713  		ID string
   714  	}
   715  }
   716  
   717  func getImageIDsFromBuild(output []byte) ([]string, error) {
   718  	var ids []string
   719  	for _, line := range bytes.Split(output, []byte("\n")) {
   720  		if len(line) == 0 {
   721  			continue
   722  		}
   723  		entry := buildLine{}
   724  		if err := json.Unmarshal(line, &entry); err != nil {
   725  			return nil, err
   726  		}
   727  		if entry.Aux.ID != "" {
   728  			ids = append(ids, entry.Aux.ID)
   729  		}
   730  	}
   731  	return ids, nil
   732  }