github.com/rawahars/moby@v24.0.4+incompatible/integration-cli/docker_api_build_test.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"regexp"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/testutil/fakecontext"
    17  	"github.com/docker/docker/testutil/fakegit"
    18  	"github.com/docker/docker/testutil/fakestorage"
    19  	"github.com/docker/docker/testutil/request"
    20  	"gotest.tools/v3/assert"
    21  	is "gotest.tools/v3/assert/cmp"
    22  )
    23  
    24  func (s *DockerAPISuite) TestBuildAPIDockerFileRemote(c *testing.T) {
    25  	testRequires(c, NotUserNamespace)
    26  
    27  	// -xdev is required because sysfs can cause EPERM
    28  	testD := `FROM busybox
    29  RUN find / -xdev -name ba*
    30  RUN find /tmp/`
    31  	server := fakestorage.New(c, "", fakecontext.WithFiles(map[string]string{"testD": testD}))
    32  	defer server.Close()
    33  
    34  	res, body, err := request.Post("/build?dockerfile=baz&remote="+server.URL()+"/testD", request.JSON)
    35  	assert.NilError(c, err)
    36  	assert.Equal(c, res.StatusCode, http.StatusOK)
    37  
    38  	buf, err := request.ReadBody(body)
    39  	assert.NilError(c, err)
    40  
    41  	// Make sure Dockerfile exists.
    42  	// Make sure 'baz' doesn't exist ANYWHERE despite being mentioned in the URL
    43  	out := string(buf)
    44  	assert.Assert(c, is.Contains(out, "RUN find /tmp"))
    45  	assert.Assert(c, !strings.Contains(out, "baz"))
    46  }
    47  
    48  func (s *DockerAPISuite) TestBuildAPIRemoteTarballContext(c *testing.T) {
    49  	buffer := new(bytes.Buffer)
    50  	tw := tar.NewWriter(buffer)
    51  	defer tw.Close()
    52  
    53  	dockerfile := []byte("FROM busybox")
    54  	err := tw.WriteHeader(&tar.Header{
    55  		Name: "Dockerfile",
    56  		Size: int64(len(dockerfile)),
    57  	})
    58  	assert.NilError(c, err, "failed to write tar file header")
    59  
    60  	_, err = tw.Write(dockerfile)
    61  	assert.NilError(c, err, "failed to write tar file content")
    62  	assert.NilError(c, tw.Close(), "failed to close tar archive")
    63  
    64  	server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
    65  		"testT.tar": buffer,
    66  	}))
    67  	defer server.Close()
    68  
    69  	res, b, err := request.Post("/build?remote="+server.URL()+"/testT.tar", request.ContentType("application/tar"))
    70  	assert.NilError(c, err)
    71  	assert.Equal(c, res.StatusCode, http.StatusOK)
    72  	b.Close()
    73  }
    74  
    75  func (s *DockerAPISuite) TestBuildAPIRemoteTarballContextWithCustomDockerfile(c *testing.T) {
    76  	buffer := new(bytes.Buffer)
    77  	tw := tar.NewWriter(buffer)
    78  	defer tw.Close()
    79  
    80  	dockerfile := []byte(`FROM busybox
    81  RUN echo 'wrong'`)
    82  	err := tw.WriteHeader(&tar.Header{
    83  		Name: "Dockerfile",
    84  		Size: int64(len(dockerfile)),
    85  	})
    86  	// failed to write tar file header
    87  	assert.NilError(c, err)
    88  
    89  	_, err = tw.Write(dockerfile)
    90  	// failed to write tar file content
    91  	assert.NilError(c, err)
    92  
    93  	custom := []byte(`FROM busybox
    94  RUN echo 'right'
    95  `)
    96  	err = tw.WriteHeader(&tar.Header{
    97  		Name: "custom",
    98  		Size: int64(len(custom)),
    99  	})
   100  
   101  	// failed to write tar file header
   102  	assert.NilError(c, err)
   103  
   104  	_, err = tw.Write(custom)
   105  	// failed to write tar file content
   106  	assert.NilError(c, err)
   107  
   108  	// failed to close tar archive
   109  	assert.NilError(c, tw.Close())
   110  
   111  	server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
   112  		"testT.tar": buffer,
   113  	}))
   114  	defer server.Close()
   115  
   116  	url := "/build?dockerfile=custom&remote=" + server.URL() + "/testT.tar"
   117  	res, body, err := request.Post(url, request.ContentType("application/tar"))
   118  	assert.NilError(c, err)
   119  	assert.Equal(c, res.StatusCode, http.StatusOK)
   120  
   121  	defer body.Close()
   122  	content, err := request.ReadBody(body)
   123  	assert.NilError(c, err)
   124  
   125  	// Build used the wrong dockerfile.
   126  	assert.Assert(c, !strings.Contains(string(content), "wrong"))
   127  }
   128  
   129  func (s *DockerAPISuite) TestBuildAPILowerDockerfile(c *testing.T) {
   130  	git := fakegit.New(c, "repo", map[string]string{
   131  		"dockerfile": `FROM busybox
   132  RUN echo from dockerfile`,
   133  	}, false)
   134  	defer git.Close()
   135  
   136  	res, body, err := request.Post("/build?remote="+git.RepoURL, request.JSON)
   137  	assert.NilError(c, err)
   138  	assert.Equal(c, res.StatusCode, http.StatusOK)
   139  
   140  	buf, err := request.ReadBody(body)
   141  	assert.NilError(c, err)
   142  
   143  	out := string(buf)
   144  	assert.Assert(c, is.Contains(out, "from dockerfile"))
   145  }
   146  
   147  func (s *DockerAPISuite) TestBuildAPIBuildGitWithF(c *testing.T) {
   148  	git := fakegit.New(c, "repo", map[string]string{
   149  		"baz": `FROM busybox
   150  RUN echo from baz`,
   151  		"Dockerfile": `FROM busybox
   152  RUN echo from Dockerfile`,
   153  	}, false)
   154  	defer git.Close()
   155  
   156  	// Make sure it tries to 'dockerfile' query param value
   157  	res, body, err := request.Post("/build?dockerfile=baz&remote="+git.RepoURL, request.JSON)
   158  	assert.NilError(c, err)
   159  	assert.Equal(c, res.StatusCode, http.StatusOK)
   160  
   161  	buf, err := request.ReadBody(body)
   162  	assert.NilError(c, err)
   163  
   164  	out := string(buf)
   165  	assert.Assert(c, is.Contains(out, "from baz"))
   166  }
   167  
   168  func (s *DockerAPISuite) TestBuildAPIDoubleDockerfile(c *testing.T) {
   169  	testRequires(c, UnixCli) // dockerfile overwrites Dockerfile on Windows
   170  	git := fakegit.New(c, "repo", map[string]string{
   171  		"Dockerfile": `FROM busybox
   172  RUN echo from Dockerfile`,
   173  		"dockerfile": `FROM busybox
   174  RUN echo from dockerfile`,
   175  	}, false)
   176  	defer git.Close()
   177  
   178  	// Make sure it tries to 'dockerfile' query param value
   179  	res, body, err := request.Post("/build?remote="+git.RepoURL, request.JSON)
   180  	assert.NilError(c, err)
   181  	assert.Equal(c, res.StatusCode, http.StatusOK)
   182  
   183  	buf, err := request.ReadBody(body)
   184  	assert.NilError(c, err)
   185  
   186  	out := string(buf)
   187  	assert.Assert(c, is.Contains(out, "from Dockerfile"))
   188  }
   189  
   190  func (s *DockerAPISuite) TestBuildAPIUnnormalizedTarPaths(c *testing.T) {
   191  	// Make sure that build context tars with entries of the form
   192  	// x/./y don't cause caching false positives.
   193  
   194  	buildFromTarContext := func(fileContents []byte) string {
   195  		buffer := new(bytes.Buffer)
   196  		tw := tar.NewWriter(buffer)
   197  		defer tw.Close()
   198  
   199  		dockerfile := []byte(`FROM busybox
   200  	COPY dir /dir/`)
   201  		err := tw.WriteHeader(&tar.Header{
   202  			Name: "Dockerfile",
   203  			Size: int64(len(dockerfile)),
   204  		})
   205  		assert.NilError(c, err, "failed to write tar file header")
   206  
   207  		_, err = tw.Write(dockerfile)
   208  		assert.NilError(c, err, "failed to write Dockerfile in tar file content")
   209  
   210  		err = tw.WriteHeader(&tar.Header{
   211  			Name: "dir/./file",
   212  			Size: int64(len(fileContents)),
   213  		})
   214  		assert.NilError(c, err, "failed to write tar file header")
   215  
   216  		_, err = tw.Write(fileContents)
   217  		assert.NilError(c, err, "failed to write file contents in tar file content")
   218  
   219  		assert.NilError(c, tw.Close(), "failed to close tar archive")
   220  
   221  		res, body, err := request.Post("/build", request.RawContent(io.NopCloser(buffer)), request.ContentType("application/x-tar"))
   222  		assert.NilError(c, err)
   223  		assert.Equal(c, res.StatusCode, http.StatusOK)
   224  
   225  		out, err := request.ReadBody(body)
   226  		assert.NilError(c, err)
   227  		lines := strings.Split(string(out), "\n")
   228  		assert.Assert(c, len(lines) > 1)
   229  		matched, err := regexp.MatchString(".*Successfully built [0-9a-f]{12}.*", lines[len(lines)-2])
   230  		assert.NilError(c, err)
   231  		assert.Assert(c, matched)
   232  
   233  		re := regexp.MustCompile("Successfully built ([0-9a-f]{12})")
   234  		matches := re.FindStringSubmatch(lines[len(lines)-2])
   235  		return matches[1]
   236  	}
   237  
   238  	imageA := buildFromTarContext([]byte("abc"))
   239  	imageB := buildFromTarContext([]byte("def"))
   240  
   241  	assert.Assert(c, imageA != imageB)
   242  }
   243  
   244  func (s *DockerAPISuite) TestBuildOnBuildWithCopy(c *testing.T) {
   245  	dockerfile := `
   246  		FROM ` + minimalBaseImage() + ` as onbuildbase
   247  		ONBUILD COPY file /file
   248  
   249  		FROM onbuildbase
   250  	`
   251  	ctx := fakecontext.New(c, "",
   252  		fakecontext.WithDockerfile(dockerfile),
   253  		fakecontext.WithFile("file", "some content"),
   254  	)
   255  	defer ctx.Close()
   256  
   257  	res, body, err := request.Post(
   258  		"/build",
   259  		request.RawContent(ctx.AsTarReader(c)),
   260  		request.ContentType("application/x-tar"))
   261  	assert.NilError(c, err)
   262  	assert.Equal(c, res.StatusCode, http.StatusOK)
   263  
   264  	out, err := request.ReadBody(body)
   265  	assert.NilError(c, err)
   266  	assert.Assert(c, is.Contains(string(out), "Successfully built"))
   267  }
   268  
   269  func (s *DockerAPISuite) TestBuildOnBuildCache(c *testing.T) {
   270  	build := func(dockerfile string) []byte {
   271  		ctx := fakecontext.New(c, "",
   272  			fakecontext.WithDockerfile(dockerfile),
   273  		)
   274  		defer ctx.Close()
   275  
   276  		res, body, err := request.Post(
   277  			"/build",
   278  			request.RawContent(ctx.AsTarReader(c)),
   279  			request.ContentType("application/x-tar"))
   280  		assert.NilError(c, err)
   281  		assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
   282  
   283  		out, err := request.ReadBody(body)
   284  		assert.NilError(c, err)
   285  		assert.Assert(c, is.Contains(string(out), "Successfully built"))
   286  		return out
   287  	}
   288  
   289  	dockerfile := `
   290  		FROM ` + minimalBaseImage() + ` as onbuildbase
   291  		ENV something=bar
   292  		ONBUILD ENV foo=bar
   293  	`
   294  	build(dockerfile)
   295  
   296  	dockerfile += "FROM onbuildbase"
   297  	out := build(dockerfile)
   298  
   299  	imageIDs := getImageIDsFromBuild(c, out)
   300  	assert.Assert(c, is.Len(imageIDs, 2))
   301  	parentID, childID := imageIDs[0], imageIDs[1]
   302  
   303  	client := testEnv.APIClient()
   304  
   305  	// check parentID is correct
   306  	image, _, err := client.ImageInspectWithRaw(context.Background(), childID)
   307  	assert.NilError(c, err)
   308  	assert.Check(c, is.Equal(parentID, image.Parent))
   309  }
   310  
   311  func (s *DockerRegistrySuite) TestBuildCopyFromForcePull(c *testing.T) {
   312  	client := testEnv.APIClient()
   313  
   314  	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
   315  	// tag the image to upload it to the private registry
   316  	err := client.ImageTag(context.TODO(), "busybox", repoName)
   317  	assert.Check(c, err)
   318  	// push the image to the registry
   319  	rc, err := client.ImagePush(context.TODO(), repoName, types.ImagePushOptions{RegistryAuth: "{}"})
   320  	assert.Check(c, err)
   321  	_, err = io.Copy(io.Discard, rc)
   322  	assert.Check(c, err)
   323  
   324  	dockerfile := fmt.Sprintf(`
   325  		FROM %s AS foo
   326  		RUN touch abc
   327  		FROM %s
   328  		COPY --from=foo /abc /
   329  		`, repoName, repoName)
   330  
   331  	ctx := fakecontext.New(c, "",
   332  		fakecontext.WithDockerfile(dockerfile),
   333  	)
   334  	defer ctx.Close()
   335  
   336  	res, body, err := request.Post(
   337  		"/build?pull=1",
   338  		request.RawContent(ctx.AsTarReader(c)),
   339  		request.ContentType("application/x-tar"))
   340  	assert.NilError(c, err)
   341  	assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
   342  
   343  	out, err := request.ReadBody(body)
   344  	assert.NilError(c, err)
   345  	assert.Check(c, is.Contains(string(out), "Successfully built"))
   346  }
   347  
   348  func (s *DockerAPISuite) TestBuildAddRemoteNoDecompress(c *testing.T) {
   349  	buffer := new(bytes.Buffer)
   350  	tw := tar.NewWriter(buffer)
   351  	dt := []byte("contents")
   352  	err := tw.WriteHeader(&tar.Header{
   353  		Name:     "foo",
   354  		Size:     int64(len(dt)),
   355  		Mode:     0600,
   356  		Typeflag: tar.TypeReg,
   357  	})
   358  	assert.NilError(c, err)
   359  	_, err = tw.Write(dt)
   360  	assert.NilError(c, err)
   361  	err = tw.Close()
   362  	assert.NilError(c, err)
   363  
   364  	server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
   365  		"test.tar": buffer,
   366  	}))
   367  	defer server.Close()
   368  
   369  	dockerfile := fmt.Sprintf(`
   370  		FROM busybox
   371  		ADD %s/test.tar /
   372  		RUN [ -f test.tar ]
   373  		`, server.URL())
   374  
   375  	ctx := fakecontext.New(c, "",
   376  		fakecontext.WithDockerfile(dockerfile),
   377  	)
   378  	defer ctx.Close()
   379  
   380  	res, body, err := request.Post(
   381  		"/build",
   382  		request.RawContent(ctx.AsTarReader(c)),
   383  		request.ContentType("application/x-tar"))
   384  	assert.NilError(c, err)
   385  	assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
   386  
   387  	out, err := request.ReadBody(body)
   388  	assert.NilError(c, err)
   389  	assert.Check(c, is.Contains(string(out), "Successfully built"))
   390  }
   391  
   392  func (s *DockerAPISuite) TestBuildChownOnCopy(c *testing.T) {
   393  	// new feature added in 1.31 - https://github.com/moby/moby/pull/34263
   394  	testRequires(c, DaemonIsLinux, MinimumAPIVersion("1.31"))
   395  	dockerfile := `FROM busybox
   396  		RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
   397  		RUN echo 'test1:x:1001:' >> /etc/group
   398  		RUN echo 'test2:x:1002:' >> /etc/group
   399  		COPY --chown=test1:1002 . /new_dir
   400  		RUN ls -l /
   401  		RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]
   402  		RUN [ $(ls -nl / | grep new_dir | awk '{print $3":"$4}') = '1001:1002' ]
   403  	`
   404  	ctx := fakecontext.New(c, "",
   405  		fakecontext.WithDockerfile(dockerfile),
   406  		fakecontext.WithFile("test_file1", "some test content"),
   407  	)
   408  	defer ctx.Close()
   409  
   410  	res, body, err := request.Post(
   411  		"/build",
   412  		request.RawContent(ctx.AsTarReader(c)),
   413  		request.ContentType("application/x-tar"))
   414  	assert.NilError(c, err)
   415  	assert.Equal(c, res.StatusCode, http.StatusOK)
   416  
   417  	out, err := request.ReadBody(body)
   418  	assert.NilError(c, err)
   419  	assert.Check(c, is.Contains(string(out), "Successfully built"))
   420  }
   421  
   422  func (s *DockerAPISuite) TestBuildCopyCacheOnFileChange(c *testing.T) {
   423  	dockerfile := `FROM busybox
   424  COPY file /file`
   425  
   426  	ctx1 := fakecontext.New(c, "",
   427  		fakecontext.WithDockerfile(dockerfile),
   428  		fakecontext.WithFile("file", "foo"))
   429  	ctx2 := fakecontext.New(c, "",
   430  		fakecontext.WithDockerfile(dockerfile),
   431  		fakecontext.WithFile("file", "bar"))
   432  
   433  	var build = func(ctx *fakecontext.Fake) string {
   434  		res, body, err := request.Post("/build",
   435  			request.RawContent(ctx.AsTarReader(c)),
   436  			request.ContentType("application/x-tar"))
   437  
   438  		assert.NilError(c, err)
   439  		assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
   440  
   441  		out, err := request.ReadBody(body)
   442  		assert.NilError(c, err)
   443  		assert.Assert(c, is.Contains(string(out), "Successfully built"))
   444  
   445  		ids := getImageIDsFromBuild(c, out)
   446  		assert.Assert(c, is.Len(ids, 1))
   447  		return ids[len(ids)-1]
   448  	}
   449  
   450  	id1 := build(ctx1)
   451  	id2 := build(ctx1)
   452  	id3 := build(ctx2)
   453  
   454  	if id1 != id2 {
   455  		c.Fatal("didn't use the cache")
   456  	}
   457  	if id1 == id3 {
   458  		c.Fatal("COPY With different source file should not share same cache")
   459  	}
   460  }
   461  
   462  func (s *DockerAPISuite) TestBuildAddCacheOnFileChange(c *testing.T) {
   463  	dockerfile := `FROM busybox
   464  ADD file /file`
   465  
   466  	ctx1 := fakecontext.New(c, "",
   467  		fakecontext.WithDockerfile(dockerfile),
   468  		fakecontext.WithFile("file", "foo"))
   469  	ctx2 := fakecontext.New(c, "",
   470  		fakecontext.WithDockerfile(dockerfile),
   471  		fakecontext.WithFile("file", "bar"))
   472  
   473  	var build = func(ctx *fakecontext.Fake) string {
   474  		res, body, err := request.Post("/build",
   475  			request.RawContent(ctx.AsTarReader(c)),
   476  			request.ContentType("application/x-tar"))
   477  
   478  		assert.NilError(c, err)
   479  		assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
   480  
   481  		out, err := request.ReadBody(body)
   482  		assert.NilError(c, err)
   483  		assert.Assert(c, is.Contains(string(out), "Successfully built"))
   484  
   485  		ids := getImageIDsFromBuild(c, out)
   486  		assert.Assert(c, is.Len(ids, 1))
   487  		return ids[len(ids)-1]
   488  	}
   489  
   490  	id1 := build(ctx1)
   491  	id2 := build(ctx1)
   492  	id3 := build(ctx2)
   493  
   494  	if id1 != id2 {
   495  		c.Fatal("didn't use the cache")
   496  	}
   497  	if id1 == id3 {
   498  		c.Fatal("COPY With different source file should not share same cache")
   499  	}
   500  }
   501  
   502  func (s *DockerAPISuite) TestBuildScratchCopy(c *testing.T) {
   503  	testRequires(c, DaemonIsLinux)
   504  	dockerfile := `FROM scratch
   505  ADD Dockerfile /
   506  ENV foo bar`
   507  	ctx := fakecontext.New(c, "",
   508  		fakecontext.WithDockerfile(dockerfile),
   509  	)
   510  	defer ctx.Close()
   511  
   512  	res, body, err := request.Post(
   513  		"/build",
   514  		request.RawContent(ctx.AsTarReader(c)),
   515  		request.ContentType("application/x-tar"))
   516  	assert.NilError(c, err)
   517  	assert.Equal(c, res.StatusCode, http.StatusOK)
   518  
   519  	out, err := request.ReadBody(body)
   520  	assert.NilError(c, err)
   521  	assert.Check(c, is.Contains(string(out), "Successfully built"))
   522  }
   523  
   524  type buildLine struct {
   525  	Stream string
   526  	Aux    struct {
   527  		ID string
   528  	}
   529  }
   530  
   531  func getImageIDsFromBuild(c *testing.T, output []byte) []string {
   532  	var ids []string
   533  	for _, line := range bytes.Split(output, []byte("\n")) {
   534  		if len(line) == 0 {
   535  			continue
   536  		}
   537  		entry := buildLine{}
   538  		assert.NilError(c, json.Unmarshal(line, &entry))
   539  		if entry.Aux.ID != "" {
   540  			ids = append(ids, entry.Aux.ID)
   541  		}
   542  	}
   543  	return ids
   544  }