Files
moby/integration-cli/docker_api_build_test.go
Sebastiaan van Stijn 3388108f9a api/types/image: InspectResponse: deprecate Parent, DockerVersion
The image inspect response has various fields that were deprecated as
part of the legacy builder, or Dockerfile syntax;

- The `Parent` field is only used for the legacy builder, and only set for
  images that are built locally (i.e., not persisted when pulling an image).
- The `DockerVersion` field is only set when building images with the legacy
  builder, and empty in most cases.

This patch deprecates the fields in the `InspectResponse` go struct, as
these fields will no longer be set in future once the legacy builder is
removed (`Parent`, `DockerVersion`). The legacy builder's deprecation in
[cli@4d8e457] / [cli@fd22746] (docker 23.0, API v1.42), however the related
API fields were kept so that information of legacy images would not be
discarded.

The API continues to return these fields if set, allowing the client to
print the fields for informational purposes when printing the raw response,
but these fields should be considered "transitional", and not be depended
on; deprecating the fields helps raise awareness.

[cli@4d8e457]: 4d8e45782b
[cli@fd22746]: fd2274692f

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit bd8a99b400)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-05 23:06:38 +02:00

572 lines
15 KiB
Go

package main
import (
"archive/tar"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"testing"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/fakecontext"
"github.com/docker/docker/testutil/fakegit"
"github.com/docker/docker/testutil/fakestorage"
"github.com/docker/docker/testutil/request"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func (s *DockerAPISuite) TestBuildAPIDockerFileRemote(c *testing.T) {
testRequires(c, NotUserNamespace)
ctx := testutil.GetContext(c)
// -xdev is required because sysfs can cause EPERM
testD := `FROM busybox
RUN find / -xdev -name ba*
RUN find /tmp/`
server := fakestorage.New(c, "", fakecontext.WithFiles(map[string]string{"testD": testD}))
defer server.Close()
res, body, err := request.Post(ctx, "/build?dockerfile=baz&remote="+server.URL()+"/testD", request.JSON)
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(c, err)
// Make sure Dockerfile exists.
// Make sure 'baz' doesn't exist ANYWHERE despite being mentioned in the URL
out := string(buf)
assert.Assert(c, is.Contains(out, "RUN find /tmp"))
assert.Assert(c, !strings.Contains(out, "baz"))
}
func (s *DockerAPISuite) TestBuildAPIRemoteTarballContext(c *testing.T) {
ctx := testutil.GetContext(c)
buffer := new(bytes.Buffer)
tw := tar.NewWriter(buffer)
defer tw.Close()
dockerfile := []byte("FROM busybox")
err := tw.WriteHeader(&tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
})
assert.NilError(c, err, "failed to write tar file header")
_, err = tw.Write(dockerfile)
assert.NilError(c, err, "failed to write tar file content")
assert.NilError(c, tw.Close(), "failed to close tar archive")
server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
"testT.tar": buffer,
}))
defer server.Close()
res, b, err := request.Post(ctx, "/build?remote="+server.URL()+"/testT.tar", request.ContentType("application/tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
b.Close()
}
func (s *DockerAPISuite) TestBuildAPIRemoteTarballContextWithCustomDockerfile(c *testing.T) {
buffer := new(bytes.Buffer)
tw := tar.NewWriter(buffer)
defer tw.Close()
dockerfile := []byte(`FROM busybox
RUN echo 'wrong'`)
err := tw.WriteHeader(&tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
})
// failed to write tar file header
assert.NilError(c, err)
_, err = tw.Write(dockerfile)
// failed to write tar file content
assert.NilError(c, err)
custom := []byte(`FROM busybox
RUN echo 'right'
`)
err = tw.WriteHeader(&tar.Header{
Name: "custom",
Size: int64(len(custom)),
})
// failed to write tar file header
assert.NilError(c, err)
_, err = tw.Write(custom)
// failed to write tar file content
assert.NilError(c, err)
// failed to close tar archive
assert.NilError(c, tw.Close())
server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
"testT.tar": buffer,
}))
defer server.Close()
ctx := testutil.GetContext(c)
url := "/build?dockerfile=custom&remote=" + server.URL() + "/testT.tar"
res, body, err := request.Post(ctx, url, request.ContentType("application/tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
content, err := request.ReadBody(body)
assert.NilError(c, err)
// Build used the wrong dockerfile.
assert.Assert(c, !strings.Contains(string(content), "wrong"))
}
func (s *DockerAPISuite) TestBuildAPILowerDockerfile(c *testing.T) {
git := fakegit.New(c, "repo", map[string]string{
"dockerfile": `FROM busybox
RUN echo from dockerfile`,
}, false)
defer git.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(ctx, "/build?remote="+git.RepoURL, request.JSON)
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(c, err)
out := string(buf)
assert.Assert(c, is.Contains(out, "from dockerfile"))
}
func (s *DockerAPISuite) TestBuildAPIBuildGitWithF(c *testing.T) {
git := fakegit.New(c, "repo", map[string]string{
"baz": `FROM busybox
RUN echo from baz`,
"Dockerfile": `FROM busybox
RUN echo from Dockerfile`,
}, false)
defer git.Close()
ctx := testutil.GetContext(c)
// Make sure it tries to 'dockerfile' query param value
res, body, err := request.Post(ctx, "/build?dockerfile=baz&remote="+git.RepoURL, request.JSON)
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(c, err)
out := string(buf)
assert.Assert(c, is.Contains(out, "from baz"))
}
func (s *DockerAPISuite) TestBuildAPIDoubleDockerfile(c *testing.T) {
testRequires(c, UnixCli) // dockerfile overwrites Dockerfile on Windows
git := fakegit.New(c, "repo", map[string]string{
"Dockerfile": `FROM busybox
RUN echo from Dockerfile`,
"dockerfile": `FROM busybox
RUN echo from dockerfile`,
}, false)
defer git.Close()
ctx := testutil.GetContext(c)
// Make sure it tries to 'dockerfile' query param value
res, body, err := request.Post(ctx, "/build?remote="+git.RepoURL, request.JSON)
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
buf, err := request.ReadBody(body)
assert.NilError(c, err)
out := string(buf)
assert.Assert(c, is.Contains(out, "from Dockerfile"))
}
func (s *DockerAPISuite) TestBuildAPIUnnormalizedTarPaths(c *testing.T) {
// Make sure that build context tars with entries of the form
// x/./y don't cause caching false positives.
buildFromTarContext := func(fileContents []byte) string {
buffer := new(bytes.Buffer)
tw := tar.NewWriter(buffer)
defer tw.Close()
dockerfile := []byte(`FROM busybox
COPY dir /dir/`)
err := tw.WriteHeader(&tar.Header{
Name: "Dockerfile",
Size: int64(len(dockerfile)),
})
assert.NilError(c, err, "failed to write tar file header")
_, err = tw.Write(dockerfile)
assert.NilError(c, err, "failed to write Dockerfile in tar file content")
err = tw.WriteHeader(&tar.Header{
Name: "dir/./file",
Size: int64(len(fileContents)),
})
assert.NilError(c, err, "failed to write tar file header")
_, err = tw.Write(fileContents)
assert.NilError(c, err, "failed to write file contents in tar file content")
assert.NilError(c, tw.Close(), "failed to close tar archive")
ctx := testutil.GetContext(c)
res, body, err := request.Post(ctx, "/build", request.RawContent(io.NopCloser(buffer)), request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
out, err := request.ReadBody(body)
assert.NilError(c, err)
lines := strings.Split(string(out), "\n")
assert.Assert(c, len(lines) > 1)
matched, err := regexp.MatchString(".*Successfully built [0-9a-f]{12}.*", lines[len(lines)-2])
assert.NilError(c, err)
assert.Assert(c, matched)
re := regexp.MustCompile("Successfully built ([0-9a-f]{12})")
matches := re.FindStringSubmatch(lines[len(lines)-2])
return matches[1]
}
imageA := buildFromTarContext([]byte("abc"))
imageB := buildFromTarContext([]byte("def"))
assert.Assert(c, imageA != imageB)
}
func (s *DockerAPISuite) TestBuildOnBuildWithCopy(c *testing.T) {
dockerfile := `
FROM ` + minimalBaseImage() + ` as onbuildbase
ONBUILD COPY file /file
FROM onbuildbase
`
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("file", "some content"),
)
defer bCtx.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(
ctx,
"/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Assert(c, is.Contains(string(out), "Successfully built"))
}
func (s *DockerAPISuite) TestBuildOnBuildCache(c *testing.T) {
build := func(dockerfile string) []byte {
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
)
defer bCtx.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(
ctx,
"/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Assert(c, is.Contains(string(out), "Successfully built"))
return out
}
dockerfile := `
FROM ` + minimalBaseImage() + ` as onbuildbase
ENV something=bar
ONBUILD ENV foo=bar
`
build(dockerfile)
dockerfile += "FROM onbuildbase"
out := build(dockerfile)
imageIDs := getImageIDsFromBuild(c, out)
assert.Assert(c, is.Len(imageIDs, 2))
parentID, childID := imageIDs[0], imageIDs[1]
apiClient := testEnv.APIClient()
ctx := testutil.GetContext(c)
// check parentID is correct
// Parent is graphdriver-only
if !testEnv.UsingSnapshotter() {
image, err := apiClient.ImageInspect(ctx, childID)
assert.NilError(c, err)
assert.Check(c, is.Equal(parentID, image.Parent)) //nolint:staticcheck // ignore SA1019: field is deprecated, but still included in response when present.
}
}
func (s *DockerRegistrySuite) TestBuildCopyFromForcePull(c *testing.T) {
client := testEnv.APIClient()
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
// tag the image to upload it to the private registry
ctx := testutil.GetContext(c)
err := client.ImageTag(ctx, "busybox", repoName)
assert.Check(c, err)
// push the image to the registry
rc, err := client.ImagePush(ctx, repoName, image.PushOptions{RegistryAuth: "{}"})
assert.Check(c, err)
_, err = io.Copy(io.Discard, rc)
assert.Check(c, err)
dockerfile := fmt.Sprintf(`
FROM %s AS foo
RUN touch abc
FROM %s
COPY --from=foo /abc /
`, repoName, repoName)
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
)
defer bCtx.Close()
res, body, err := request.Post(
ctx,
"/build?pull=1",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Check(c, is.Contains(string(out), "Successfully built"))
}
func (s *DockerAPISuite) TestBuildAddRemoteNoDecompress(c *testing.T) {
buffer := new(bytes.Buffer)
tw := tar.NewWriter(buffer)
dt := []byte("contents")
err := tw.WriteHeader(&tar.Header{
Name: "foo",
Size: int64(len(dt)),
Mode: 0o600,
Typeflag: tar.TypeReg,
})
assert.NilError(c, err)
_, err = tw.Write(dt)
assert.NilError(c, err)
err = tw.Close()
assert.NilError(c, err)
server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{
"test.tar": buffer,
}))
defer server.Close()
dockerfile := fmt.Sprintf(`
FROM busybox
ADD %s/test.tar /
RUN [ -f test.tar ]
`, server.URL())
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
)
defer bCtx.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(
ctx,
"/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Check(c, is.Contains(string(out), "Successfully built"))
}
func (s *DockerAPISuite) TestBuildChownOnCopy(c *testing.T) {
testRequires(c, DaemonIsLinux)
dockerfile := `FROM busybox
RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
RUN echo 'test1:x:1001:' >> /etc/group
RUN echo 'test2:x:1002:' >> /etc/group
COPY --chown=test1:1002 . /new_dir
RUN ls -l /
RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]
RUN [ $(ls -nl / | grep new_dir | awk '{print $3":"$4}') = '1001:1002' ]
`
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("test_file1", "some test content"),
)
defer bCtx.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(
ctx,
"/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Check(c, is.Contains(string(out), "Successfully built"))
}
func (s *DockerAPISuite) TestBuildCopyCacheOnFileChange(c *testing.T) {
dockerfile := `FROM busybox
COPY file /file`
ctx1 := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("file", "foo"))
ctx2 := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("file", "bar"))
ctx := testutil.GetContext(c)
build := func(bCtx *fakecontext.Fake) string {
res, body, err := request.Post(ctx, "/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Assert(c, is.Contains(string(out), "Successfully built"))
ids := getImageIDsFromBuild(c, out)
assert.Assert(c, is.Len(ids, 1))
return ids[len(ids)-1]
}
id1 := build(ctx1)
id2 := build(ctx1)
id3 := build(ctx2)
if id1 != id2 {
c.Fatal("didn't use the cache")
}
if id1 == id3 {
c.Fatal("COPY With different source file should not share same cache")
}
}
func (s *DockerAPISuite) TestBuildAddCacheOnFileChange(c *testing.T) {
dockerfile := `FROM busybox
ADD file /file`
ctx1 := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("file", "foo"))
ctx2 := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
fakecontext.WithFile("file", "bar"))
ctx := testutil.GetContext(c)
build := func(bCtx *fakecontext.Fake) string {
res, body, err := request.Post(ctx, "/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode))
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Assert(c, is.Contains(string(out), "Successfully built"))
ids := getImageIDsFromBuild(c, out)
assert.Assert(c, is.Len(ids, 1))
return ids[len(ids)-1]
}
id1 := build(ctx1)
id2 := build(ctx1)
id3 := build(ctx2)
if id1 != id2 {
c.Fatal("didn't use the cache")
}
if id1 == id3 {
c.Fatal("COPY With different source file should not share same cache")
}
}
func (s *DockerAPISuite) TestBuildScratchCopy(c *testing.T) {
testRequires(c, DaemonIsLinux)
dockerfile := `FROM scratch
ADD Dockerfile /
ENV foo bar`
bCtx := fakecontext.New(c, "",
fakecontext.WithDockerfile(dockerfile),
)
defer bCtx.Close()
ctx := testutil.GetContext(c)
res, body, err := request.Post(
ctx,
"/build",
request.RawContent(bCtx.AsTarReader(c)),
request.ContentType("application/x-tar"))
assert.NilError(c, err)
assert.Equal(c, res.StatusCode, http.StatusOK)
out, err := request.ReadBody(body)
assert.NilError(c, err)
assert.Check(c, is.Contains(string(out), "Successfully built"))
}
type buildLine struct {
Stream string
Aux struct {
ID string
}
}
func getImageIDsFromBuild(t *testing.T, output []byte) []string {
var ids []string
for _, line := range bytes.Split(output, []byte("\n")) {
if len(line) == 0 {
continue
}
entry := buildLine{}
assert.NilError(t, json.Unmarshal(line, &entry))
if entry.Aux.ID != "" {
ids = append(ids, entry.Aux.ID)
}
}
return ids
}