github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/integration/build/build_test.go (about)

     1  package build // import "github.com/demonoid81/moby/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/demonoid81/moby/api/types"
    14  	"github.com/demonoid81/moby/api/types/filters"
    15  	"github.com/demonoid81/moby/api/types/versions"
    16  	"github.com/demonoid81/moby/errdefs"
    17  	"github.com/demonoid81/moby/pkg/jsonmessage"
    18  	"github.com/demonoid81/moby/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  	resp, err := apiclient.ImageBuild(ctx,
   211  		source.AsTarReader(t),
   212  		types.ImageBuildOptions{
   213  			Remove:      true,
   214  			ForceRemove: true,
   215  			Tags:        []string{"build1"},
   216  		})
   217  	assert.NilError(t, err)
   218  	_, err = io.Copy(ioutil.Discard, resp.Body)
   219  	resp.Body.Close()
   220  	assert.NilError(t, err)
   221  
   222  	image, _, err := apiclient.ImageInspectWithRaw(ctx, "build1")
   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  	bldName := "build-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{bldName},
   264  			Labels:      testLabels,
   265  			Target:      "target-a",
   266  		})
   267  	assert.NilError(t, err)
   268  	_, err = io.Copy(ioutil.Discard, resp.Body)
   269  	resp.Body.Close()
   270  	assert.NilError(t, err)
   271  
   272  	image, _, err := apiclient.ImageInspectWithRaw(ctx, bldName)
   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  	bldName = "build-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{bldName},
   291  			Labels:      testLabels,
   292  			Target:      "target-b",
   293  		})
   294  	assert.NilError(t, err)
   295  	_, err = io.Copy(ioutil.Discard, resp.Body)
   296  	resp.Body.Close()
   297  	assert.NilError(t, err)
   298  
   299  	image, _, err = apiclient.ImageInspectWithRaw(ctx, bldName)
   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(ioutil.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  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   344  	defer setupTest(t)()
   345  	// test both metadata and layer based commands as they may be implemented differently
   346  	dockerfile := `FROM busybox AS stage1
   347  ONBUILD RUN echo 'foo' >somefile
   348  ONBUILD ENV bar=baz
   349  
   350  FROM stage1
   351  # fails if ONBUILD RUN fails
   352  RUN cat somefile
   353  
   354  FROM stage1
   355  RUN cat somefile`
   356  
   357  	ctx := context.Background()
   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.Check(t, is.Equal(3, len(imageIDs)))
   381  
   382  	image, _, err := apiclient.ImageInspectWithRaw(context.Background(), 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 := context.TODO()
   393  	defer setupTest(t)()
   394  
   395  	dockerfile := `FROM scratch
   396  COPY foo /
   397  FROM scratch
   398  COPY bar /`
   399  
   400  	buf := bytes.NewBuffer(nil)
   401  	w := tar.NewWriter(buf)
   402  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   403  	writeTarRecord(t, w, "../foo", "foocontents0")
   404  	writeTarRecord(t, w, "/bar", "barcontents0")
   405  	err := w.Close()
   406  	assert.NilError(t, err)
   407  
   408  	apiclient := testEnv.APIClient()
   409  	resp, err := apiclient.ImageBuild(ctx,
   410  		buf,
   411  		types.ImageBuildOptions{
   412  			Remove:      true,
   413  			ForceRemove: true,
   414  		})
   415  
   416  	out := bytes.NewBuffer(nil)
   417  	assert.NilError(t, err)
   418  	_, err = io.Copy(out, resp.Body)
   419  	resp.Body.Close()
   420  	assert.NilError(t, err)
   421  
   422  	// repeat with changed data should not cause cache hits
   423  
   424  	buf = bytes.NewBuffer(nil)
   425  	w = tar.NewWriter(buf)
   426  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   427  	writeTarRecord(t, w, "../foo", "foocontents1")
   428  	writeTarRecord(t, w, "/bar", "barcontents1")
   429  	err = w.Close()
   430  	assert.NilError(t, err)
   431  
   432  	resp, err = apiclient.ImageBuild(ctx,
   433  		buf,
   434  		types.ImageBuildOptions{
   435  			Remove:      true,
   436  			ForceRemove: true,
   437  		})
   438  
   439  	out = bytes.NewBuffer(nil)
   440  	assert.NilError(t, err)
   441  	_, err = io.Copy(out, resp.Body)
   442  	resp.Body.Close()
   443  	assert.NilError(t, err)
   444  	assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
   445  }
   446  
   447  // docker/for-linux#135
   448  // #35641
   449  func TestBuildMultiStageLayerLeak(t *testing.T) {
   450  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   451  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
   452  	ctx := context.TODO()
   453  	defer setupTest(t)()
   454  
   455  	// all commands need to match until COPY
   456  	dockerfile := `FROM busybox
   457  WORKDIR /foo
   458  COPY foo .
   459  FROM busybox
   460  WORKDIR /foo
   461  COPY bar .
   462  RUN [ -f bar ]
   463  RUN [ ! -f foo ]
   464  `
   465  
   466  	source := fakecontext.New(t, "",
   467  		fakecontext.WithFile("foo", "0"),
   468  		fakecontext.WithFile("bar", "1"),
   469  		fakecontext.WithDockerfile(dockerfile))
   470  	defer source.Close()
   471  
   472  	apiclient := testEnv.APIClient()
   473  	resp, err := apiclient.ImageBuild(ctx,
   474  		source.AsTarReader(t),
   475  		types.ImageBuildOptions{
   476  			Remove:      true,
   477  			ForceRemove: true,
   478  		})
   479  
   480  	out := bytes.NewBuffer(nil)
   481  	assert.NilError(t, err)
   482  	_, err = io.Copy(out, resp.Body)
   483  	resp.Body.Close()
   484  	assert.NilError(t, err)
   485  
   486  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   487  }
   488  
   489  // #37581
   490  func TestBuildWithHugeFile(t *testing.T) {
   491  	skip.If(t, testEnv.OSType == "windows")
   492  	ctx := context.TODO()
   493  	defer setupTest(t)()
   494  
   495  	dockerfile := `FROM busybox
   496  # create a sparse file with size over 8GB
   497  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 && \
   498      ls -la rnd && du -sk rnd`
   499  
   500  	buf := bytes.NewBuffer(nil)
   501  	w := tar.NewWriter(buf)
   502  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   503  	err := w.Close()
   504  	assert.NilError(t, err)
   505  
   506  	apiclient := testEnv.APIClient()
   507  	resp, err := apiclient.ImageBuild(ctx,
   508  		buf,
   509  		types.ImageBuildOptions{
   510  			Remove:      true,
   511  			ForceRemove: true,
   512  		})
   513  
   514  	out := bytes.NewBuffer(nil)
   515  	assert.NilError(t, err)
   516  	_, err = io.Copy(out, resp.Body)
   517  	resp.Body.Close()
   518  	assert.NilError(t, err)
   519  	assert.Check(t, is.Contains(out.String(), "Successfully built"))
   520  }
   521  
   522  func TestBuildWithEmptyDockerfile(t *testing.T) {
   523  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   524  	ctx := context.TODO()
   525  	defer setupTest(t)()
   526  
   527  	tests := []struct {
   528  		name        string
   529  		dockerfile  string
   530  		expectedErr string
   531  	}{
   532  		{
   533  			name:        "empty-dockerfile",
   534  			dockerfile:  "",
   535  			expectedErr: "cannot be empty",
   536  		},
   537  		{
   538  			name: "empty-lines-dockerfile",
   539  			dockerfile: `
   540  			
   541  			
   542  			
   543  			`,
   544  			expectedErr: "file with no instructions",
   545  		},
   546  		{
   547  			name:        "comment-only-dockerfile",
   548  			dockerfile:  `# this is a comment`,
   549  			expectedErr: "file with no instructions",
   550  		},
   551  	}
   552  
   553  	apiclient := testEnv.APIClient()
   554  
   555  	for _, tc := range tests {
   556  		tc := tc
   557  		t.Run(tc.name, func(t *testing.T) {
   558  			t.Parallel()
   559  
   560  			buf := bytes.NewBuffer(nil)
   561  			w := tar.NewWriter(buf)
   562  			writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
   563  			err := w.Close()
   564  			assert.NilError(t, err)
   565  
   566  			_, err = apiclient.ImageBuild(ctx,
   567  				buf,
   568  				types.ImageBuildOptions{
   569  					Remove:      true,
   570  					ForceRemove: true,
   571  				})
   572  
   573  			assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
   574  		})
   575  	}
   576  }
   577  
   578  func TestBuildPreserveOwnership(t *testing.T) {
   579  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
   580  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
   581  
   582  	ctx := context.Background()
   583  
   584  	dockerfile, err := ioutil.ReadFile("testdata/Dockerfile.testBuildPreserveOwnership")
   585  	assert.NilError(t, err)
   586  
   587  	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
   588  	defer source.Close()
   589  
   590  	apiclient := testEnv.APIClient()
   591  
   592  	for _, target := range []string{"copy_from", "copy_from_chowned"} {
   593  		t.Run(target, func(t *testing.T) {
   594  			resp, err := apiclient.ImageBuild(
   595  				ctx,
   596  				source.AsTarReader(t),
   597  				types.ImageBuildOptions{
   598  					Remove:      true,
   599  					ForceRemove: true,
   600  					Target:      target,
   601  				},
   602  			)
   603  			assert.NilError(t, err)
   604  
   605  			out := bytes.NewBuffer(nil)
   606  			_, err = io.Copy(out, resp.Body)
   607  			_ = resp.Body.Close()
   608  			if err != nil {
   609  				t.Log(out)
   610  			}
   611  			assert.NilError(t, err)
   612  		})
   613  	}
   614  }
   615  
   616  func TestBuildPlatformInvalid(t *testing.T) {
   617  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
   618  
   619  	ctx := context.Background()
   620  	defer setupTest(t)()
   621  
   622  	dockerfile := `FROM busybox
   623  `
   624  
   625  	buf := bytes.NewBuffer(nil)
   626  	w := tar.NewWriter(buf)
   627  	writeTarRecord(t, w, "Dockerfile", dockerfile)
   628  	err := w.Close()
   629  	assert.NilError(t, err)
   630  
   631  	apiclient := testEnv.APIClient()
   632  	_, err = apiclient.ImageBuild(ctx,
   633  		buf,
   634  		types.ImageBuildOptions{
   635  			Remove:      true,
   636  			ForceRemove: true,
   637  			Platform:    "foobar",
   638  		})
   639  
   640  	assert.Assert(t, err != nil)
   641  	assert.ErrorContains(t, err, "unknown operating system or architecture")
   642  	assert.Assert(t, errdefs.IsInvalidParameter(err))
   643  }
   644  
   645  func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
   646  	err := w.WriteHeader(&tar.Header{
   647  		Name:     fn,
   648  		Mode:     0600,
   649  		Size:     int64(len(contents)),
   650  		Typeflag: '0',
   651  	})
   652  	assert.NilError(t, err)
   653  	_, err = w.Write([]byte(contents))
   654  	assert.NilError(t, err)
   655  }
   656  
   657  type buildLine struct {
   658  	Stream string
   659  	Aux    struct {
   660  		ID string
   661  	}
   662  }
   663  
   664  func getImageIDsFromBuild(output []byte) ([]string, error) {
   665  	var ids []string
   666  	for _, line := range bytes.Split(output, []byte("\n")) {
   667  		if len(line) == 0 {
   668  			continue
   669  		}
   670  		entry := buildLine{}
   671  		if err := json.Unmarshal(line, &entry); err != nil {
   672  			return nil, err
   673  		}
   674  		if entry.Aux.ID != "" {
   675  			ids = append(ids, entry.Aux.ID)
   676  		}
   677  	}
   678  	return ids, nil
   679  }