gopkg.in/docker/docker.v20@v20.10.27/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 TestBuildWithEmptyDockerfile(t *testing.T) {
   528  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   529  	ctx := context.TODO()
   530  	defer setupTest(t)()
   531  
   532  	tests := []struct {
   533  		name        string
   534  		dockerfile  string
   535  		expectedErr string
   536  	}{
   537  		{
   538  			name:        "empty-dockerfile",
   539  			dockerfile:  "",
   540  			expectedErr: "cannot be empty",
   541  		},
   542  		{
   543  			name: "empty-lines-dockerfile",
   544  			dockerfile: `
   545  
   546  
   547  
   548  			`,
   549  			expectedErr: "file with no instructions",
   550  		},
   551  		{
   552  			name:        "comment-only-dockerfile",
   553  			dockerfile:  `# this is a comment`,
   554  			expectedErr: "file with no instructions",
   555  		},
   556  	}
   557  
   558  	apiclient := testEnv.APIClient()
   559  
   560  	for _, tc := range tests {
   561  		tc := tc
   562  		t.Run(tc.name, func(t *testing.T) {
   563  			t.Parallel()
   564  
   565  			buf := bytes.NewBuffer(nil)
   566  			w := tar.NewWriter(buf)
   567  			writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
   568  			err := w.Close()
   569  			assert.NilError(t, err)
   570  
   571  			_, err = apiclient.ImageBuild(ctx,
   572  				buf,
   573  				types.ImageBuildOptions{
   574  					Remove:      true,
   575  					ForceRemove: true,
   576  				})
   577  
   578  			assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
   579  		})
   580  	}
   581  }
   582  
   583  func TestBuildPreserveOwnership(t *testing.T) {
   584  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   585  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   586  
   587  	ctx := context.Background()
   588  
   589  	dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name())
   590  	assert.NilError(t, err)
   591  
   592  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
   593  	defer source.Close()
   594  
   595  	apiclient := testEnv.APIClient()
   596  
   597  	for _, target := range []string{"copy_from", "copy_from_chowned"} {
   598  		t.Run(target, func(t *testing.T) {
   599  			resp, err := apiclient.ImageBuild(
   600  				ctx,
   601  				source.AsTarReader(t),
   602  				types.ImageBuildOptions{
   603  					Remove:      true,
   604  					ForceRemove: true,
   605  					Target:      target,
   606  				},
   607  			)
   608  			assert.NilError(t, err)
   609  
   610  			out := bytes.NewBuffer(nil)
   611  			_, err = io.Copy(out, resp.Body)
   612  			_ = resp.Body.Close()
   613  			if err != nil {
   614  				t.Log(out)
   615  			}
   616  			assert.NilError(t, err)
   617  		})
   618  	}
   619  }
   620  
   621  func TestBuildPlatformInvalid(t *testing.T) {
   622  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
   623  
   624  	ctx := context.Background()
   625  	defer setupTest(t)()
   626  
   627  	dockerfile := `FROM busybox
   628  `
   629  
   630  	buf := bytes.NewBuffer(nil)
   631  	w := tar.NewWriter(buf)
   632  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   633  	err := w.Close()
   634  	assert.NilError(t, err)
   635  
   636  	apiclient := testEnv.APIClient()
   637  	_, err = apiclient.ImageBuild(ctx,
   638  		buf,
   639  		types.ImageBuildOptions{
   640  			Remove:      true,
   641  			ForceRemove: true,
   642  			Platform:    "foobar",
   643  		})
   644  
   645  	assert.Assert(t, err != nil)
   646  	assert.ErrorContains(t, err, "unknown operating system or architecture")
   647  	assert.Assert(t, errdefs.IsInvalidParameter(err))
   648  }
   649  
   650  func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
   651  	err := w.WriteHeader(&tar.Header{
   652  		Name:     fn,
   653  		Mode:     0600,
   654  		Size:     int64(len(contents)),
   655  		Typeflag: '0',
   656  	})
   657  	assert.NilError(t, err)
   658  	_, err = w.Write([]byte(contents))
   659  	assert.NilError(t, err)
   660  }
   661  
   662  type buildLine struct {
   663  	Stream string
   664  	Aux    struct {
   665  		ID string
   666  	}
   667  }
   668  
   669  func getImageIDsFromBuild(output []byte) ([]string, error) {
   670  	var ids []string
   671  	for _, line := range bytes.Split(output, []byte("\n")) {
   672  		if len(line) == 0 {
   673  			continue
   674  		}
   675  		entry := buildLine{}
   676  		if err := json.Unmarshal(line, &entry); err != nil {
   677  			return nil, err
   678  		}
   679  		if entry.Aux.ID != "" {
   680  			ids = append(ids, entry.Aux.ID)
   681  		}
   682  	}
   683  	return ids, nil
   684  }