github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+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  	"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  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
    26  	defer 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  	ctx := context.Background()
    93  	for _, c := range cases {
    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 := ioutil.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  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   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 := context.Background()
   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(ioutil.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 := context.Background()
   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(ioutil.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(ioutil.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 := context.Background()
   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(ioutil.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  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   345  	defer setupTest(t)()
   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  	ctx := context.Background()
   359  	source := fakecontext.New(t, "",
   360  		fakecontext.WithDockerfile(dockerfile))
   361  	defer source.Close()
   362  
   363  	apiclient := testEnv.APIClient()
   364  	resp, err := apiclient.ImageBuild(ctx,
   365  		source.AsTarReader(t),
   366  		types.ImageBuildOptions{
   367  			Remove:      true,
   368  			ForceRemove: true,
   369  		})
   370  
   371  	out := bytes.NewBuffer(nil)
   372  	assert.NilError(t, err)
   373  	_, err = io.Copy(out, resp.Body)
   374  	resp.Body.Close()
   375  	assert.NilError(t, err)
   376  
   377  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   378  
   379  	imageIDs, err := getImageIDsFromBuild(out.Bytes())
   380  	assert.NilError(t, err)
   381  	assert.Assert(t, is.Equal(3, len(imageIDs)))
   382  
   383  	image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2])
   384  	assert.NilError(t, err)
   385  	assert.Check(t, is.Contains(image.Config.Env, "bar=baz"))
   386  }
   387  
   388  // #35403 #36122
   389  func TestBuildUncleanTarFilenames(t *testing.T) {
   390  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
   391  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   392  
   393  	ctx := context.TODO()
   394  	defer setupTest(t)()
   395  
   396  	dockerfile := `FROM scratch
   397  COPY foo /
   398  FROM scratch
   399  COPY bar /`
   400  
   401  	buf := bytes.NewBuffer(nil)
   402  	w := tar.NewWriter(buf)
   403  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   404  	writeTarRecord(t, w, "../foo", "foocontents0")
   405  	writeTarRecord(t, w, "/bar", "barcontents0")
   406  	err := w.Close()
   407  	assert.NilError(t, err)
   408  
   409  	apiclient := testEnv.APIClient()
   410  	resp, err := apiclient.ImageBuild(ctx,
   411  		buf,
   412  		types.ImageBuildOptions{
   413  			Remove:      true,
   414  			ForceRemove: true,
   415  		})
   416  
   417  	out := bytes.NewBuffer(nil)
   418  	assert.NilError(t, err)
   419  	_, err = io.Copy(out, resp.Body)
   420  	resp.Body.Close()
   421  	assert.NilError(t, err)
   422  
   423  	// repeat with changed data should not cause cache hits
   424  
   425  	buf = bytes.NewBuffer(nil)
   426  	w = tar.NewWriter(buf)
   427  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   428  	writeTarRecord(t, w, "../foo", "foocontents1")
   429  	writeTarRecord(t, w, "/bar", "barcontents1")
   430  	err = w.Close()
   431  	assert.NilError(t, err)
   432  
   433  	resp, err = apiclient.ImageBuild(ctx,
   434  		buf,
   435  		types.ImageBuildOptions{
   436  			Remove:      true,
   437  			ForceRemove: true,
   438  		})
   439  
   440  	out = bytes.NewBuffer(nil)
   441  	assert.NilError(t, err)
   442  	_, err = io.Copy(out, resp.Body)
   443  	resp.Body.Close()
   444  	assert.NilError(t, err)
   445  	assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
   446  }
   447  
   448  // docker/for-linux#135
   449  // #35641
   450  func TestBuildMultiStageLayerLeak(t *testing.T) {
   451  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   452  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
   453  	ctx := context.TODO()
   454  	defer setupTest(t)()
   455  
   456  	// all commands need to match until COPY
   457  	dockerfile := `FROM busybox
   458  WORKDIR /foo
   459  COPY foo .
   460  FROM busybox
   461  WORKDIR /foo
   462  COPY bar .
   463  RUN [ -f bar ]
   464  RUN [ ! -f foo ]
   465  `
   466  
   467  	source := fakecontext.New(t, "",
   468  		fakecontext.WithFile("foo", "0"),
   469  		fakecontext.WithFile("bar", "1"),
   470  		fakecontext.WithDockerfile(dockerfile))
   471  	defer source.Close()
   472  
   473  	apiclient := testEnv.APIClient()
   474  	resp, err := apiclient.ImageBuild(ctx,
   475  		source.AsTarReader(t),
   476  		types.ImageBuildOptions{
   477  			Remove:      true,
   478  			ForceRemove: true,
   479  		})
   480  
   481  	out := bytes.NewBuffer(nil)
   482  	assert.NilError(t, err)
   483  	_, err = io.Copy(out, resp.Body)
   484  	resp.Body.Close()
   485  	assert.NilError(t, err)
   486  
   487  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   488  }
   489  
   490  // #37581
   491  func TestBuildWithHugeFile(t *testing.T) {
   492  	skip.If(t, testEnv.OSType == "windows")
   493  	ctx := context.TODO()
   494  	defer setupTest(t)()
   495  
   496  	dockerfile := `FROM busybox
   497  # create a sparse file with size over 8GB
   498  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 && \
   499      ls -la rnd && du -sk rnd`
   500  
   501  	buf := bytes.NewBuffer(nil)
   502  	w := tar.NewWriter(buf)
   503  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   504  	err := w.Close()
   505  	assert.NilError(t, err)
   506  
   507  	apiclient := testEnv.APIClient()
   508  	resp, err := apiclient.ImageBuild(ctx,
   509  		buf,
   510  		types.ImageBuildOptions{
   511  			Remove:      true,
   512  			ForceRemove: true,
   513  		})
   514  
   515  	out := bytes.NewBuffer(nil)
   516  	assert.NilError(t, err)
   517  	_, err = io.Copy(out, resp.Body)
   518  	resp.Body.Close()
   519  	assert.NilError(t, err)
   520  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   521  }
   522  
   523  func TestBuildWithEmptyDockerfile(t *testing.T) {
   524  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   525  	ctx := context.TODO()
   526  	defer setupTest(t)()
   527  
   528  	tests := []struct {
   529  		name        string
   530  		dockerfile  string
   531  		expectedErr string
   532  	}{
   533  		{
   534  			name:        "empty-dockerfile",
   535  			dockerfile:  "",
   536  			expectedErr: "cannot be empty",
   537  		},
   538  		{
   539  			name: "empty-lines-dockerfile",
   540  			dockerfile: `
   541  			
   542  			
   543  			
   544  			`,
   545  			expectedErr: "file with no instructions",
   546  		},
   547  		{
   548  			name:        "comment-only-dockerfile",
   549  			dockerfile:  `# this is a comment`,
   550  			expectedErr: "file with no instructions",
   551  		},
   552  	}
   553  
   554  	apiclient := testEnv.APIClient()
   555  
   556  	for _, tc := range tests {
   557  		tc := tc
   558  		t.Run(tc.name, func(t *testing.T) {
   559  			t.Parallel()
   560  
   561  			buf := bytes.NewBuffer(nil)
   562  			w := tar.NewWriter(buf)
   563  			writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
   564  			err := w.Close()
   565  			assert.NilError(t, err)
   566  
   567  			_, err = apiclient.ImageBuild(ctx,
   568  				buf,
   569  				types.ImageBuildOptions{
   570  					Remove:      true,
   571  					ForceRemove: true,
   572  				})
   573  
   574  			assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
   575  		})
   576  	}
   577  }
   578  
   579  func TestBuildPreserveOwnership(t *testing.T) {
   580  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   581  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   582  
   583  	ctx := context.Background()
   584  
   585  	dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
   586  	assert.NilError(t, err)
   587  
   588  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
   589  	defer source.Close()
   590  
   591  	apiclient := testEnv.APIClient()
   592  
   593  	for _, target := range []string{"copy_from", "copy_from_chowned"} {
   594  		t.Run(target, func(t *testing.T) {
   595  			resp, err := apiclient.ImageBuild(
   596  				ctx,
   597  				source.AsTarReader(t),
   598  				types.ImageBuildOptions{
   599  					Remove:      true,
   600  					ForceRemove: true,
   601  					Target:      target,
   602  				},
   603  			)
   604  			assert.NilError(t, err)
   605  
   606  			out := bytes.NewBuffer(nil)
   607  			_, err = io.Copy(out, resp.Body)
   608  			_ = resp.Body.Close()
   609  			if err != nil {
   610  				t.Log(out)
   611  			}
   612  			assert.NilError(t, err)
   613  		})
   614  	}
   615  }
   616  
   617  func TestBuildPlatformInvalid(t *testing.T) {
   618  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
   619  
   620  	ctx := context.Background()
   621  	defer setupTest(t)()
   622  
   623  	dockerfile := `FROM busybox
   624  `
   625  
   626  	buf := bytes.NewBuffer(nil)
   627  	w := tar.NewWriter(buf)
   628  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   629  	err := w.Close()
   630  	assert.NilError(t, err)
   631  
   632  	apiclient := testEnv.APIClient()
   633  	_, err = apiclient.ImageBuild(ctx,
   634  		buf,
   635  		types.ImageBuildOptions{
   636  			Remove:      true,
   637  			ForceRemove: true,
   638  			Platform:    "foobar",
   639  		})
   640  
   641  	assert.Assert(t, err != nil)
   642  	assert.ErrorContains(t, err, "unknown operating system or architecture")
   643  	assert.Assert(t, errdefs.IsInvalidParameter(err))
   644  }
   645  
   646  func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
   647  	err := w.WriteHeader(&tar.Header{
   648  		Name:     fn,
   649  		Mode:     0600,
   650  		Size:     int64(len(contents)),
   651  		Typeflag: '0',
   652  	})
   653  	assert.NilError(t, err)
   654  	_, err = w.Write([]byte(contents))
   655  	assert.NilError(t, err)
   656  }
   657  
   658  type buildLine struct {
   659  	Stream string
   660  	Aux    struct {
   661  		ID string
   662  	}
   663  }
   664  
   665  func getImageIDsFromBuild(output []byte) ([]string, error) {
   666  	var ids []string
   667  	for _, line := range bytes.Split(output, []byte("\n")) {
   668  		if len(line) == 0 {
   669  			continue
   670  		}
   671  		entry := buildLine{}
   672  		if err := json.Unmarshal(line, &entry); err != nil {
   673  			return nil, err
   674  		}
   675  		if entry.Aux.ID != "" {
   676  			ids = append(ids, entry.Aux.ID)
   677  		}
   678  	}
   679  	return ids, nil
   680  }