github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/integration/build/build_test.go (about)

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