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