Files
buildx/tests/build.go
T
Paweł Gronowski 21de7d2078 build: Fix iidfile for containerd-backed Docker driver
Images loaded into Docker's containerd image store are identified by
their manifest or index digest, while the legacy graphdriver store uses
the config digest.

buildx prefers containerimage.config.digest when the Moby exporter
returns both digests.
Some exporter responses still produce a usable iidfile, but affected
builds, such as those with attestations disabled, write a config digest
that Docker image commands cannot resolve.

Expose the Docker driver's image-store mode as a feature and set
prefer-image-digest for containerd-backed Moby exports.

Signed-off-by: Paweł Gronowski <git@grono.dev>
2026-07-13 20:01:34 +02:00

1849 lines
55 KiB
Go

package tests
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/containerd/continuity/fs/fstest"
"github.com/containerd/platforms"
"github.com/creack/pty"
"github.com/docker/buildx/localstate"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/gitutil"
"github.com/docker/buildx/util/gitutil/gittestutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/ociindex"
"github.com/moby/buildkit/frontend/subrequests/lint"
"github.com/moby/buildkit/frontend/subrequests/outline"
"github.com/moby/buildkit/frontend/subrequests/targets"
"github.com/moby/buildkit/identity"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/util/appdefaults"
"github.com/moby/buildkit/util/contentutil"
bkgitutil "github.com/moby/buildkit/util/gitutil"
"github.com/moby/buildkit/util/testutil"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func buildCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) {
opts = append([]cmdOpt{withArgs("build", "--progress=quiet")}, opts...)
cmd := buildxCmd(sb, opts...)
out, err := cmd.CombinedOutput()
return string(out), err
}
var buildTests = []func(t *testing.T, sb integration.Sandbox){
testBuild,
testBuildAlias,
testBuildStdin,
testBuildRemote,
testBuildRemoteAuth,
testBuildNamedContextOCILayoutDigestOnly,
testBuildLocalState,
testBuildLocalStateStdin,
testBuildLocalStateRemote,
testImageIDOutput,
testBuildLocalExport,
testBuildLocalExportDeleteMode,
testBuildRegistryExport,
testBuildRegistryExportAttestations,
testBuildTarExport,
testBuildMobyFromLocalImage,
testBuildDetailsLink,
testBuildProgress,
testBuildAnnotations,
testBuildBuildArgNoKey,
testBuildBuildKitSyntaxEmpty,
testBuildLabelNoKey,
testBuildCacheExportNotSupported,
testBuildOCIExportNotSupported,
testBuildMultiPlatform,
testDockerHostGateway,
testBuildNetworkModeBridge,
testBuildShmSize,
testBuildUlimit,
testBuildMetadataProvenance,
testBuildMetadataProvenanceMultiplatform,
testBuildMetadataWarnings,
testBuildMultiExporters,
testBuildLoadPush,
testBuildSecret,
testBuildDefaultLoad,
testBuildCall,
testBuildCheckCallOutput,
testBuildExtraHosts,
testBuildIndexAnnotationsLoadDocker,
}
func testBuild(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
out, err := buildCmd(sb, withArgs(dir))
require.NoError(t, err, string(out))
}
func testBuildAlias(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
cmd := buildxCmd(sb, withDir(dir), withArgs("b", dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
}
func testBuildStdin(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(
t,
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("build", "--progress=quiet", "-f-", dir))
cmd.Stdin = bytes.NewReader(dockerfile)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
}
func testBuildRemote(t *testing.T, sb integration.Sandbox) {
t.Run("default branch", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
addr := gittestutil.GitServeHTTP(git, t)
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dirDest, addr))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
})
t.Run("tag ref with url fragment", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
gittestutil.GitTag(git, t, "v0.1.0")
addr := gittestutil.GitServeHTTP(git, t)
addr = addr + "#v0.1.0" // tag
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dirDest, addr))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
})
t.Run("tag ref with query string", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
gittestutil.GitTag(git, t, "v0.1.0")
addr := gittestutil.GitServeHTTP(git, t)
addr = addr + "?tag=v0.1.0" // tag
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dirDest, addr))
if matchesBuildKitVersion(t, sb, ">= 0.24.0-0") {
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
} else {
require.Error(t, err)
require.Contains(t, out, "current frontend does not support Git URLs with query string components")
}
})
t.Run("tag ref with query string frontend 1.17", func(t *testing.T) {
dockerfile := []byte(`
# syntax=docker/dockerfile:1.17
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
gittestutil.GitTag(git, t, "v0.1.0")
addr := gittestutil.GitServeHTTP(git, t)
addr = addr + "?tag=v0.1.0" // tag
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dirDest, addr))
if matchesBuildKitVersion(t, sb, ">= 0.24.0-0") {
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
} else {
require.Error(t, err)
require.Contains(t, out, "current frontend does not support Git URLs with query string components")
}
})
t.Run("tag ref with query string frontend 1.18.0", func(t *testing.T) {
dockerfile := []byte(`
# syntax=docker/dockerfile-upstream:1.18.0
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
gittestutil.GitTag(git, t, "v0.1.0")
addr := gittestutil.GitServeHTTP(git, t)
addr = addr + "?tag=v0.1.0" // tag
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dirDest, addr))
if matchesBuildKitVersion(t, sb, ">= 0.24.0-0") {
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
} else {
require.Error(t, err)
require.Contains(t, out, "current frontend does not support Git URLs with query string components")
}
})
}
func testBuildRemoteAuth(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
token := identity.NewID()
addr := gittestutil.GitServeHTTP(git, t, gittestutil.WithAccessToken(token))
out, err := buildCmd(sb, withDir(dir),
withEnv("GIT_AUTH_TOKEN="+token),
withArgs(
"--secret", "id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN",
"--output=type=local,dest="+dirDest,
addr,
),
)
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
}
func testBuildNamedContextOCILayoutDigestOnly(t *testing.T, sb integration.Sandbox) {
if isMobyWorker(sb) {
t.Skip("oci-layout named contexts are not supported by the docker worker")
}
dir := tmpdir(t, fstest.CreateFile("Dockerfile", []byte(`
FROM scratch
COPY --from=proxy /foo /foo
`), 0o600))
layoutPath := filepath.Join(dir, "layout")
expected := "from-oci-layout"
manifestDigest := createOCILayoutImage(t, layoutPath, "foo", []byte(expected), "latest")
dirDest := t.TempDir()
out, err := buildCmd(sb,
withDir(dir),
withArgs(
"--build-context", "proxy=oci-layout://layout@"+manifestDigest.String(),
"--output=type=local,dest="+dirDest,
dir,
),
)
require.NoError(t, err, out)
dt, err := os.ReadFile(filepath.Join(dirDest, "foo"))
require.NoError(t, err)
require.Equal(t, expected, string(dt))
}
func testBuildLocalState(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(
t,
fstest.CreateFile("build.Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
out, err := buildCmd(sb, withDir(dir), withArgs(
"-f", "build.Dockerfile",
"--metadata-file", filepath.Join(dir, "md.json"),
".",
))
require.NoError(t, err, out)
dt, err := os.ReadFile(filepath.Join(dir, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
require.NoError(t, err)
refParts := strings.Split(md.BuildRef, "/")
require.Len(t, refParts, 3)
ref, err := ls.ReadRef(refParts[0], refParts[1], refParts[2])
require.NoError(t, err)
require.NotNil(t, ref)
require.DirExists(t, ref.LocalPath)
require.FileExists(t, ref.DockerfilePath)
}
func testBuildLocalStateStdin(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(
t,
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("build", "--progress=quiet", "--metadata-file", filepath.Join(dir, "md.json"), "-f-", dir))
cmd.Stdin = bytes.NewReader(dockerfile)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
require.NoError(t, err)
refParts := strings.Split(md.BuildRef, "/")
require.Len(t, refParts, 3)
ref, err := ls.ReadRef(refParts[0], refParts[1], refParts[2])
require.NoError(t, err)
require.NotNil(t, ref)
require.DirExists(t, ref.LocalPath)
require.Equal(t, "-", ref.DockerfilePath)
}
func testBuildLocalStateRemote(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox:latest
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("build.Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
dirDest := t.TempDir()
git, err := gitutil.New(bkgitutil.WithDir(dir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "build.Dockerfile", "foo")
gittestutil.GitCommit(git, t, "initial commit")
addr := gittestutil.GitServeHTTP(git, t)
out, err := buildCmd(sb, withDir(dir), withArgs(
"-f", "build.Dockerfile",
"--metadata-file", filepath.Join(dirDest, "md.json"),
"--output", "type=local,dest="+dirDest,
addr,
))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dirDest, "foo"))
dt, err := os.ReadFile(filepath.Join(dirDest, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb))))
require.NoError(t, err)
refParts := strings.Split(md.BuildRef, "/")
require.Len(t, refParts, 3)
ref, err := ls.ReadRef(refParts[0], refParts[1], refParts[2])
require.NoError(t, err)
require.NotNil(t, ref)
require.Equal(t, addr, ref.LocalPath)
require.Equal(t, "build.Dockerfile", ref.DockerfilePath)
}
func testBuildLocalExport(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
out, err := buildCmd(sb, withArgs(fmt.Sprintf("--output=type=local,dest=%s/result", dir), dir))
require.NoError(t, err, string(out))
dt, err := os.ReadFile(dir + "/result/bar")
require.NoError(t, err)
require.Equal(t, "foo", string(dt))
}
func testBuildLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
t.Run("requires allow for current working directory", func(t *testing.T) {
dir := createTestProject(t)
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=.,mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("requires allow for outside destination", func(t *testing.T) {
dir := createTestProject(t)
dest := filepath.Join(t.TempDir(), "out")
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest="+dest+",mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("requires allow for symlink outside destination", func(t *testing.T) {
dir := createTestProject(t)
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dir, "out")))
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
require.Error(t, err, out)
require.Contains(t, out, "--allow=buildx.local.delete")
})
t.Run("allow does not accept value", func(t *testing.T) {
dir := createTestProject(t)
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete=true", "--output=type=local,dest=out", "."))
require.Error(t, err, out)
require.Contains(t, out, "buildx.local.delete does not accept a value")
})
t.Run("deletes stale files in subdirectory without allow", func(t *testing.T) {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
dir := createTestProject(t)
dest := filepath.Join(dir, "out")
stale := filepath.Join(dest, "stale")
require.NoError(t, os.MkdirAll(dest, 0o755))
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
out, err := buildCmd(sb, withDir(dir), withArgs("--output=type=local,dest=out,mode=delete", "."))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dest, "bar"))
_, err = os.Stat(stale)
require.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("allow permits current working directory", func(t *testing.T) {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "local exporter mode=delete")
dir := createTestProject(t)
stale := filepath.Join(dir, "stale")
require.NoError(t, os.WriteFile(stale, []byte("stale"), 0o600))
out, err := buildCmd(sb, withDir(dir), withArgs("--allow=buildx.local.delete", "--output=type=local,dest=.,mode=delete", "."))
require.NoError(t, err, out)
require.FileExists(t, filepath.Join(dir, "bar"))
_, err = os.Stat(stale)
require.ErrorIs(t, err, os.ErrNotExist)
})
}
func testBuildTarExport(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
outdir := path.Join(dir, "out")
out, err := buildCmd(sb, withArgs(fmt.Sprintf("--output=type=tar,dest=%s/result.tar", outdir), dir))
require.NoError(t, err, string(out))
dt, err := os.ReadFile(fmt.Sprintf("%s/result.tar", outdir))
require.NoError(t, err)
m, err := testutil.ReadTarToMap(dt, false)
require.NoError(t, err)
require.Contains(t, m, "bar")
require.Equal(t, "foo", string(m["bar"].Data))
}
func testBuildRegistryExport(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"
out, err := buildCmd(sb, withArgs(fmt.Sprintf("--output=type=image,name=%s,push=true", target), dir))
require.NoError(t, err, string(out))
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
pk := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
img := imgs.Find(pk)
require.NotNil(t, img)
require.Len(t, img.Layers, 1)
require.Equal(t, img.Layers[0]["bar"].Data, []byte("foo"))
}
func testBuildRegistryExportAttestations(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"
out, err := buildCmd(sb, withArgs(fmt.Sprintf("--output=type=image,name=%s,push=true", target), "--provenance=true", dir))
if isMobyWorker(sb) {
require.Error(t, err)
require.Contains(t, out, "Attestation is not supported")
return
} else if !isMobyContainerdSnapWorker(sb) && !matchesBuildKitVersion(t, sb, ">= 0.11.0-0") {
require.Error(t, err)
require.Contains(t, out, "Attestations are not supported by the current BuildKit daemon")
return
}
require.NoError(t, err, string(out))
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
pk := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
img := imgs.Find(pk)
require.NotNil(t, img)
require.Len(t, img.Layers, 1)
require.Equal(t, img.Layers[0]["bar"].Data, []byte("foo"))
att := imgs.FindAttestation(pk)
require.NotNil(t, att)
require.Len(t, att.Layers, 1)
}
func testImageIDOutput(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`FROM busybox:latest`)
dir := tmpdir(t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
targetDir := t.TempDir()
outFlag := "--output=type=docker"
if sb.DockerAddress() == "" {
// there is no Docker atm to load the image
outFlag += ",dest=" + targetDir + "/image.tar"
}
cmd := buildxCmd(
sb,
withArgs("build", "-q", "--provenance", "false", outFlag, "--iidfile", filepath.Join(targetDir, "iid.txt"), "--metadata-file", filepath.Join(targetDir, "md.json"), dir),
)
stdout := bytes.NewBuffer(nil)
cmd.Stdout = stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(targetDir, "iid.txt"))
require.NoError(t, err)
imageID := string(dt)
require.NotEmpty(t, imageID)
dgst, err := digest.Parse(string(dt))
require.NoError(t, err)
require.Equal(t, dgst.String(), strings.TrimSpace(stdout.String()))
// read the md.json file
dt, err = os.ReadFile(filepath.Join(targetDir, "md.json"))
require.NoError(t, err)
type mdT struct {
Digest string `json:"containerimage.digest"`
ConfigDigest string `json:"containerimage.config.digest"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
require.NotEmpty(t, md.Digest)
if !isMobyContainerdSnapWorker(sb) {
require.NotEmpty(t, md.ConfigDigest)
}
// verify the image ID output is correct
require.Contains(t, []digest.Digest{digest.Digest(md.ConfigDigest), digest.Digest(md.Digest)}, dgst)
if sb.DockerAddress() != "" {
cmd = dockerCmd(sb, withArgs("image", "inspect", imageID))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
}
}
func testBuildMobyFromLocalImage(t *testing.T, sb integration.Sandbox) {
if !isDockerWorker(sb) {
t.Skip("only testing with docker workers")
}
// pull image
cmd := dockerCmd(sb, withArgs("pull", "-q", "busybox:latest"))
stdout := bytes.NewBuffer(nil)
cmd.Stdout = stdout
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
require.Equal(t, "docker.io/library/busybox:latest", strings.TrimSpace(stdout.String()))
// create local tag
cmd = dockerCmd(sb, withArgs("tag", "busybox:latest", "buildx-test:busybox"))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
// build image
dockerfile := []byte(`FROM buildx-test:busybox`)
dir := tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd = buildxCmd(
sb,
withArgs("build", "-q", "--output=type=cacheonly", dir),
)
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
// create local tag matching a remote one
cmd = dockerCmd(sb, withArgs("tag", "busybox:latest", "busybox:1.35"))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
// build image and check that it uses the local tag
// (note: the version check should match the version of busybox in pins.go)
dockerfile = []byte(`
FROM busybox:1.35
RUN busybox | head -1 | grep v1.36.1
`)
dir = tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd = buildxCmd(
sb,
withArgs("build", "-q", "--output=type=cacheonly", dir),
)
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
}
func testBuildDetailsLink(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.11.0-0", "build details link")
buildDetailsPattern := regexp.MustCompile(`(?m)^View build details: docker-desktop://dashboard/build/[^/]+/[^/]+/[^/]+\n$`)
// build simple dockerfile
dockerfile := []byte(`FROM busybox:latest
RUN echo foo > /bar`)
dir := tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd := buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.False(t, buildDetailsPattern.MatchString(string(out)), "build details link not expected in output, got %q", out)
// create desktop-build .lastaccess file
home, err := os.UserHomeDir() // TODO: sandbox should create a temp home dir and expose it through its interface
require.NoError(t, err)
dbDir := path.Join(home, ".docker", "desktop-build")
require.NoError(t, os.MkdirAll(dbDir, 0o755))
dblaFile, err := os.Create(path.Join(dbDir, ".lastaccess"))
require.NoError(t, err)
defer func() {
dblaFile.Close()
if err := os.Remove(dblaFile.Name()); err != nil {
t.Fatal(err)
}
}()
// build again
cmd = buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
out, err = cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.True(t, buildDetailsPattern.MatchString(string(out)), "expected build details link in output, got %q", out)
// build erroneous dockerfile
dockerfile = []byte(`FROM busybox:latest
RUN exit 1`)
dir = tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd = buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
out, err = cmd.CombinedOutput()
require.Error(t, err, string(out))
require.True(t, buildDetailsPattern.MatchString(string(out)), "expected build details link in output, got %q", out)
}
func testBuildProgress(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
sbDriver, _, _ := driverName(sb.Name())
name := sb.Address()
// progress=tty
cmd := buildxCmd(sb, withArgs("build", "--progress=tty", "--output=type=cacheonly", dir))
f, err := pty.StartWithSize(cmd, &pty.Winsize{
Cols: 120,
Rows: 24,
})
require.NoError(t, err)
buf := bytes.NewBuffer(nil)
io.Copy(buf, f)
ttyOutput := buf.String()
require.Contains(t, ttyOutput, "[+] Building")
require.Contains(t, ttyOutput, fmt.Sprintf("%s:%s", sbDriver, name))
require.Contains(t, ttyOutput, "=> [internal] load build definition from Dockerfile")
require.Contains(t, ttyOutput, "=> [base 1/3] FROM docker.io/library/busybox:latest")
// progress=plain
cmd = buildxCmd(sb, withArgs("build", "--progress=plain", "--output=type=cacheonly", dir))
plainOutput, err := cmd.CombinedOutput()
require.NoError(t, err)
require.Contains(t, string(plainOutput), fmt.Sprintf(`#0 building with "%s" instance using %s driver`, name, sbDriver))
require.Contains(t, string(plainOutput), "[internal] load build definition from Dockerfile")
require.Contains(t, string(plainOutput), "[base 1/3] FROM docker.io/library/busybox:latest")
}
func testBuildAnnotations(t *testing.T, sb integration.Sandbox) {
if isMobyWorker(sb) {
t.Skip("annotations not supported on docker worker")
}
skipNoCompatBuildKit(t, sb, ">= 0.11.0-0", "annotations")
dir := createTestProject(t)
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"
annotations := []string{
"--annotation", "example1=www",
"--annotation", "index:example2=xxx",
"--annotation", "manifest:example3=yyy",
"--annotation", "manifest-descriptor[" + platforms.Format(platforms.DefaultSpec()) + "]:example4=zzz",
}
out, err := buildCmd(sb, withArgs(annotations...), withArgs(fmt.Sprintf("--output=type=image,name=%s,push=true", target), dir))
require.NoError(t, err, string(out))
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
pk := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
img := imgs.Find(pk)
require.NotNil(t, img)
require.NotNil(t, imgs.Index)
assert.Equal(t, "xxx", imgs.Index.Annotations["example2"])
require.NotNil(t, img.Manifest)
assert.Equal(t, "www", img.Manifest.Annotations["example1"])
assert.Equal(t, "yyy", img.Manifest.Annotations["example3"])
require.NotNil(t, img.Desc)
assert.Equal(t, "zzz", img.Desc.Annotations["example4"])
}
func testBuildBuildArgNoKey(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs("build", "--build-arg", "=TEST_STRING", dir))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Equal(t, `ERROR: invalid key-value pair "=TEST_STRING": empty key`, strings.TrimSpace(string(out)))
}
func testBuildBuildKitSyntaxEmpty(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs("build", "--build-arg", "BUILDKIT_SYNTAX=", dir))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), `empty BUILDKIT_SYNTAX build-arg is invalid, use --build-arg BUILDKIT_SYNTAX without '=' for optional behavior`)
}
func testBuildLabelNoKey(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs("build", "--label", "=TEST_STRING", dir))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Equal(t, `ERROR: invalid key-value pair "=TEST_STRING": empty key`, strings.TrimSpace(string(out)))
}
func testBuildCacheExportNotSupported(t *testing.T, sb integration.Sandbox) {
if !isMobyWorker(sb) {
t.Skip("only testing with docker worker")
}
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs("build", "--cache-to=type=registry", dir))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "Cache export is not supported")
}
func testBuildOCIExportNotSupported(t *testing.T, sb integration.Sandbox) {
if !isMobyWorker(sb) {
t.Skip("only testing with docker worker")
}
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs("build", fmt.Sprintf("--output=type=oci,dest=%s/result", dir), dir))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "OCI exporter is not supported")
}
func testBuildMultiPlatform(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM --platform=$BUILDPLATFORM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"
cmd := buildxCmd(sb, withArgs("build", "--platform=linux/amd64,linux/arm64", fmt.Sprintf("--output=type=image,name=%s,push=true", target), dir))
out, err := cmd.CombinedOutput()
if !isMobyWorker(sb) {
require.NoError(t, err, string(out))
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
img := imgs.Find("linux/amd64")
require.NotNil(t, img)
img = imgs.Find("linux/arm64")
require.NotNil(t, img)
} else {
require.Error(t, err, string(out))
require.Contains(t, string(out), "Multi-platform build is not supported")
}
}
func testDockerHostGateway(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox
RUN ping -c 1 buildx.host-gateway-ip.local
`)
dir := tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd := buildxCmd(sb, withArgs("build", "--add-host=buildx.host-gateway-ip.local:host-gateway", "--output=type=cacheonly", dir))
out, err := cmd.CombinedOutput()
if !isDockerWorker(sb) {
require.Error(t, err, string(out))
require.Contains(t, string(out), "host-gateway is not supported")
} else {
require.NoError(t, err, string(out))
}
}
func testBuildNetworkModeBridge(t *testing.T, sb integration.Sandbox) {
if !isDockerContainerWorker(sb) {
t.Skip("only testing with docker-container worker")
}
skipNoCompatBuildKit(t, sb, ">= 0.13.0-0", "network bridge")
var builderName string
t.Cleanup(func() {
if builderName == "" {
return
}
out, err := rmCmd(sb, withArgs(builderName))
require.NoError(t, err, out)
})
out, err := createCmd(sb, withArgs(
"--driver", "docker-container",
"--buildkitd-flags=--oci-worker-net=bridge --allow-insecure-entitlement=network.host",
))
require.NoError(t, err, out)
builderName = strings.TrimSpace(out)
dockerfile := []byte(`
FROM busybox AS build
RUN ip a show eth0 | awk '/inet / {split($2, a, "/"); print a[1]}' > /ip-bridge.txt
RUN --network=host ip a show eth0 | awk '/inet / {split($2, a, "/"); print a[1]}' > /ip-host.txt
FROM scratch
COPY --from=build /ip*.txt /`)
dir := tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd := buildxCmd(sb, withArgs("build", "--allow=network.host", fmt.Sprintf("--output=type=local,dest=%s", dir), dir))
cmd.Env = append(cmd.Env, "BUILDX_BUILDER="+builderName)
outb, err := cmd.CombinedOutput()
require.NoError(t, err, string(outb))
dt, err := os.ReadFile(filepath.Join(dir, "ip-bridge.txt"))
require.NoError(t, err)
ipBridge := net.ParseIP(strings.TrimSpace(string(dt)))
require.NotNil(t, ipBridge)
_, subnet, err := net.ParseCIDR(appdefaults.BridgeSubnet)
require.NoError(t, err)
require.True(t, subnet.Contains(ipBridge))
dt, err = os.ReadFile(filepath.Join(dir, "ip-host.txt"))
require.NoError(t, err)
ip := net.ParseIP(strings.TrimSpace(string(dt)))
require.NotNil(t, ip)
require.NotEqual(t, ip, ipBridge)
}
func testBuildShmSize(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox AS build
RUN mount | grep /dev/shm > /shmsize
FROM scratch
COPY --from=build /shmsize /
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--shm-size=128m", fmt.Sprintf("--output=type=local,dest=%s", dir), dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "shmsize"))
require.NoError(t, err)
require.Contains(t, string(dt), `size=131072k`)
}
func testBuildUlimit(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox AS build
RUN ulimit -n > first > /ulimit
FROM scratch
COPY --from=build /ulimit /
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--ulimit=nofile=1024:1024", fmt.Sprintf("--output=type=local,dest=%s", dir), dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "ulimit"))
require.NoError(t, err)
require.Contains(t, string(dt), `1024`)
}
func testBuildMetadataProvenance(t *testing.T, sb integration.Sandbox) {
t.Run("default", func(t *testing.T) {
buildMetadataProvenance(t, sb, "")
})
t.Run("max", func(t *testing.T) {
buildMetadataProvenance(t, sb, "max")
})
t.Run("min", func(t *testing.T) {
buildMetadataProvenance(t, sb, "min")
})
t.Run("disabled", func(t *testing.T) {
buildMetadataProvenance(t, sb, "disabled")
})
}
func buildMetadataProvenance(t *testing.T, sb integration.Sandbox, metadataMode string) {
dir := createTestProject(t)
dirDest := t.TempDir()
outFlag := "--output=type=docker"
if sb.DockerAddress() == "" {
// there is no Docker atm to load the image
outFlag += ",dest=" + dirDest + "/image.tar"
}
cmd := buildxCmd(
sb,
withArgs("build", outFlag, "--metadata-file", filepath.Join(dirDest, "md.json"), dir),
withEnv("BUILDX_METADATA_PROVENANCE="+metadataMode),
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dirDest, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
BuildProvenance map[string]any `json:"buildx.build.provenance"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
require.NotEmpty(t, md.BuildRef)
if metadataMode == "disabled" {
require.Empty(t, md.BuildProvenance)
return
}
require.NotEmpty(t, md.BuildProvenance)
dtprv, err := json.Marshal(md.BuildProvenance)
require.NoError(t, err)
var prv provenancetypes.ProvenancePredicateSLSA02
require.NoError(t, json.Unmarshal(dtprv, &prv))
require.Equal(t, provenancetypes.BuildKitBuildType02, prv.BuildType)
}
func testBuildMetadataProvenanceMultiplatform(t *testing.T, sb integration.Sandbox) {
t.Run("default", func(t *testing.T) {
buildMetadataProvenanceMultiplatform(t, sb, "")
})
t.Run("max", func(t *testing.T) {
buildMetadataProvenanceMultiplatform(t, sb, "max")
})
t.Run("min", func(t *testing.T) {
buildMetadataProvenanceMultiplatform(t, sb, "min")
})
t.Run("disabled", func(t *testing.T) {
buildMetadataProvenanceMultiplatform(t, sb, "disabled")
})
}
func buildMetadataProvenanceMultiplatform(t *testing.T, sb integration.Sandbox, metadataMode string) {
if isMobyWorker(sb) {
t.Skip("multi-platform build is not supported")
}
dockerfile := []byte(`
FROM --platform=$BUILDPLATFORM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"
cmd := buildxCmd(sb,
withArgs("build",
"--platform=linux/amd64,linux/arm64",
"--metadata-file", filepath.Join(dir, "md.json"),
fmt.Sprintf("--output=type=image,name=%s,push=true", target),
dir,
),
withEnv("BUILDX_METADATA_PROVENANCE="+metadataMode),
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
img := imgs.Find("linux/amd64")
require.NotNil(t, img)
img = imgs.Find("linux/arm64")
require.NotNil(t, img)
dt, err := os.ReadFile(filepath.Join(dir, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
BuildProvenanceAmd64 map[string]any `json:"buildx.build.provenance/linux/amd64"`
BuildProvenanceArm64 map[string]any `json:"buildx.build.provenance/linux/arm64"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)
require.NotEmpty(t, md.BuildRef)
if metadataMode == "disabled" {
require.Empty(t, md.BuildProvenanceAmd64)
require.Empty(t, md.BuildProvenanceArm64)
return
}
require.NotEmpty(t, md.BuildProvenanceAmd64)
require.NotEmpty(t, md.BuildProvenanceArm64)
for _, prov := range []map[string]any{md.BuildProvenanceAmd64, md.BuildProvenanceArm64} {
dtprv, err := json.Marshal(prov)
require.NoError(t, err)
var prv provenancetypes.ProvenancePredicateSLSA02
require.NoError(t, json.Unmarshal(dtprv, &prv))
require.Equal(t, provenancetypes.BuildKitBuildType02, prv.BuildType)
}
}
func testBuildMetadataWarnings(t *testing.T, sb integration.Sandbox) {
t.Run("default", func(t *testing.T) {
buildMetadataWarnings(t, sb, "")
})
t.Run("true", func(t *testing.T) {
buildMetadataWarnings(t, sb, "true")
})
t.Run("false", func(t *testing.T) {
buildMetadataWarnings(t, sb, "false")
})
}
func buildMetadataWarnings(t *testing.T, sb integration.Sandbox, mode string) {
dockerfile := []byte(`
frOM busybox as base
cOpy Dockerfile .
from scratch
COPy --from=base \
/Dockerfile \
/
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(
sb,
withArgs("build", "--metadata-file", filepath.Join(dir, "md.json"), dir),
withEnv("BUILDX_METADATA_WARNINGS="+mode),
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
BuildWarnings []client.VertexWarning `json:"buildx.build.warnings"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err, string(dt))
require.NotEmpty(t, md.BuildRef, string(dt))
if mode == "" || mode == "false" {
require.Empty(t, md.BuildWarnings, string(dt))
return
}
skipNoCompatBuildKit(t, sb, ">= 0.14.0-0", "lint")
require.Len(t, md.BuildWarnings, 3, string(dt))
}
func testBuildMultiExporters(t *testing.T, sb integration.Sandbox) {
if !isDockerContainerWorker(sb) {
t.Skip("only testing with docker-container worker")
}
skipNoCompatBuildKit(t, sb, ">= 0.13.0-0", "multi exporters")
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
targetReg := registry + "/buildx/registry:latest"
targetStore := "buildx:local-" + identity.NewID()
t.Cleanup(func() {
cmd := dockerCmd(sb, withArgs("image", "rm", targetStore))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
})
dir := createTestProject(t)
outputs := []string{
"--output", fmt.Sprintf("type=image,name=%s,push=true", targetReg),
"--output", fmt.Sprintf("type=docker,name=%s", targetStore),
"--output", fmt.Sprintf("type=oci,dest=%s/result", dir),
}
cmd := buildxCmd(sb, withArgs("build"), withArgs(outputs...), withArgs(dir))
outb, err := cmd.CombinedOutput()
require.NoError(t, err, string(outb))
// test registry
desc, provider, err := contentutil.ProviderFromRef(targetReg)
require.NoError(t, err)
_, err = testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
// test docker store
cmd = dockerCmd(sb, withArgs("image", "inspect", targetStore))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
// test oci
_, err = os.ReadFile(fmt.Sprintf("%s/result", dir))
require.NoError(t, err)
// TODO: test metadata file when supported by multi exporters https://github.com/docker/buildx/issues/2181
}
func testBuildLoadPush(t *testing.T, sb integration.Sandbox) {
if !isDockerContainerWorker(sb) {
t.Skip("only testing with docker-container worker")
}
skipNoCompatBuildKit(t, sb, ">= 0.13.0-0", "multi exporters")
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:" + identity.NewID()
t.Cleanup(func() {
cmd := dockerCmd(sb, withArgs("image", "rm", target))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
})
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs(
"build", "--push", "--load",
fmt.Sprintf("-t=%s", target),
dir,
))
outb, err := cmd.CombinedOutput()
require.NoError(t, err, string(outb))
// test registry
desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
_, err = testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
// test docker store
cmd = dockerCmd(sb, withArgs("image", "inspect", target))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
// TODO: test metadata file when supported by multi exporters https://github.com/docker/buildx/issues/2181
}
func testBuildSecret(t *testing.T, sb integration.Sandbox) {
token := "abcd1234"
dockerfile := []byte(`
FROM busybox AS build
RUN --mount=type=secret,id=token cat /run/secrets/token | tee /token
FROM scratch
COPY --from=build /token /
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("tokenfile", []byte(token), 0o600),
)
t.Run("env", func(t *testing.T) {
t.Cleanup(func() {
_ = os.Remove(filepath.Join(dir, "token"))
})
cmd := buildxCmd(sb, withEnv("TOKEN="+token), withArgs("build", "--secret=id=token,env=TOKEN", fmt.Sprintf("--output=type=local,dest=%s", dir), dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "token"))
require.NoError(t, err)
require.Equal(t, token, string(dt))
})
t.Run("file", func(t *testing.T) {
t.Cleanup(func() {
_ = os.Remove(filepath.Join(dir, "token"))
})
cmd := buildxCmd(sb, withArgs("build", "--secret=id=token,src="+path.Join(dir, "tokenfile"), fmt.Sprintf("--output=type=local,dest=%s", dir), dir))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
dt, err := os.ReadFile(filepath.Join(dir, "token"))
require.NoError(t, err)
require.Equal(t, token, string(dt))
})
}
func testBuildDefaultLoad(t *testing.T, sb integration.Sandbox) {
if !isDockerWorker(sb) {
t.Skip("only testing with docker workers")
}
tag := "buildx/build:" + identity.NewID()
var builderName string
t.Cleanup(func() {
if builderName == "" {
return
}
cmd := dockerCmd(sb, withArgs("image", "rm", tag))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
out, err := rmCmd(sb, withArgs(builderName))
require.NoError(t, err, out)
})
out, err := createCmd(sb, withArgs(
"--driver", "docker-container",
"--driver-opt", "default-load=true",
))
require.NoError(t, err, out)
builderName = strings.TrimSpace(out)
dir := createTestProject(t)
cmd := buildxCmd(sb, withArgs(
"build",
fmt.Sprintf("-t=%s", tag),
dir,
))
cmd.Env = append(cmd.Env, "BUILDX_BUILDER="+builderName)
outb, err := cmd.CombinedOutput()
require.NoError(t, err, string(outb))
cmd = dockerCmd(sb, withArgs("image", "inspect", tag))
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run())
}
func testBuildCall(t *testing.T, sb integration.Sandbox) {
t.Run("check", func(t *testing.T) {
dockerfile := []byte(`
frOM busybox as base
cOpy Dockerfile .
from scratch
COPy --from=base \
/Dockerfile \
/
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=check,format=json", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.Error(t, cmd.Run(), stdout.String(), stderr.String())
var res lint.LintResults
require.NoError(t, json.Unmarshal(stdout.Bytes(), &res))
require.Equal(t, 3, len(res.Warnings))
})
t.Run("outline", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox AS first
RUN --mount=type=secret,target=/etc/passwd,required=true --mount=type=ssh true
FROM alpine AS second
RUN --mount=type=secret,id=unused --mount=type=ssh,id=ssh2 true
FROM scratch AS third
ARG BAR
RUN --mount=type=secret,id=second${BAR} true
FROM third AS target
COPY --from=first /foo /
RUN --mount=type=ssh,id=ssh3,required true
FROM second
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--build-arg=BAR=678", "--target=target", "--call=outline,format=json", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
var res outline.Outline
require.NoError(t, json.Unmarshal(stdout.Bytes(), &res))
assert.Equal(t, "target", res.Name)
require.Equal(t, 1, len(res.Args))
assert.Equal(t, "BAR", res.Args[0].Name)
assert.Equal(t, "678", res.Args[0].Value)
require.Equal(t, 2, len(res.Secrets))
assert.Equal(t, "passwd", res.Secrets[0].Name)
assert.Equal(t, true, res.Secrets[0].Required)
assert.Equal(t, "second678", res.Secrets[1].Name)
assert.Equal(t, false, res.Secrets[1].Required)
require.Equal(t, 2, len(res.SSH))
assert.Equal(t, "default", res.SSH[0].Name)
assert.Equal(t, false, res.SSH[0].Required)
assert.Equal(t, "ssh3", res.SSH[1].Name)
assert.Equal(t, true, res.SSH[1].Required)
require.Equal(t, 1, len(res.Sources))
})
// docker/buildx#3651
t.Run("outline-quiet-output", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox
ARG FOO=bar
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
runOutline := func(args ...string) string {
cmd := buildxCmd(sb, withArgs(args...))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
return stdout.String()
}
quietOut := runOutline("build", "-q", "--call=outline", dir)
noneOut := runOutline("build", "--progress=none", "--call=outline", dir)
assert.Equal(t, noneOut, quietOut)
assert.False(t, strings.HasPrefix(quietOut, "\n"))
})
t.Run("targets", func(t *testing.T) {
dockerfile := []byte(`
# build defines stage for compiling the binary
FROM alpine AS build
RUN true
FROM busybox as second
RUN false
FROM alpine
RUN false
# binary returns the compiled binary
FROM second AS binary
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=targets,format=json", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
var res targets.List
require.NoError(t, json.Unmarshal(stdout.Bytes(), &res))
require.Equal(t, 4, len(res.Targets))
assert.Equal(t, "build", res.Targets[0].Name)
assert.Equal(t, "defines stage for compiling the binary", res.Targets[0].Description)
assert.Equal(t, "alpine", res.Targets[0].Base)
assert.Equal(t, "second", res.Targets[1].Name)
assert.Empty(t, res.Targets[1].Description)
assert.Equal(t, "busybox", res.Targets[1].Base)
assert.Empty(t, res.Targets[2].Name)
assert.Empty(t, res.Targets[2].Description)
assert.Equal(t, "alpine", res.Targets[2].Base)
assert.Equal(t, "binary", res.Targets[3].Name)
assert.Equal(t, "returns the compiled binary", res.Targets[3].Description)
assert.Equal(t, "second", res.Targets[3].Base)
assert.Equal(t, true, res.Targets[3].Default)
require.Equal(t, 1, len(res.Sources))
})
t.Run("check metadata", func(t *testing.T) {
dockerfile := []byte(`
frOM busybox as base
cOpy Dockerfile .
from scratch
COPy --from=base \
/Dockerfile \
/
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=check,format=json", "--metadata-file", filepath.Join(dir, "md.json"), dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.Error(t, cmd.Run(), stdout.String(), stderr.String())
var res lint.LintResults
require.NoError(t, json.Unmarshal(stdout.Bytes(), &res), stdout.String())
require.Len(t, res.Warnings, 3)
dt, err := os.ReadFile(filepath.Join(dir, "md.json"))
require.NoError(t, err)
type mdT struct {
BuildRef string `json:"buildx.build.ref"`
ResultJSON lint.LintResults `json:"result.json"`
}
var md mdT
require.NoError(t, json.Unmarshal(dt, &md), dt)
require.Empty(t, md.BuildRef)
require.Len(t, md.ResultJSON.Warnings, 3)
})
}
func testBuildCheckCallOutput(t *testing.T, sb integration.Sandbox) {
t.Run("check for warning count msg in check without warnings", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox AS base
COPY Dockerfile .
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=check", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
require.Contains(t, stdout.String(), "Check complete, no warnings found.")
})
t.Run("check for warning count msg in check with single warning", func(t *testing.T) {
dockerfile := []byte(`
FROM busybox as base
COPY Dockerfile .
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=check", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.Error(t, cmd.Run(), stdout.String(), stderr.String())
require.Contains(t, stdout.String(), "Check complete, 1 warning has been found!")
})
t.Run("check for warning count msg in check with multiple warnings", func(t *testing.T) {
dockerfile := []byte(`
frOM busybox as base
cOpy Dockerfile .
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
)
cmd := buildxCmd(sb, withArgs("build", "--call=check", dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.Error(t, cmd.Run(), stdout.String(), stderr.String())
require.Contains(t, stdout.String(), "Check complete, 2 warnings have been found!")
})
t.Run("check for Dockerfile path printed with context when displaying rule check warnings", func(t *testing.T) {
dockerfile := []byte(`
frOM busybox as base
cOpy Dockerfile .
`)
dir := tmpdir(
t,
fstest.CreateDir("subdir", 0o700),
fstest.CreateFile("subdir/Dockerfile", dockerfile, 0o600),
)
dockerfilePath := filepath.Join(dir, "subdir", "Dockerfile")
cmd := buildxCmd(sb, withArgs("build", "--call=check", "-f", dockerfilePath, dir))
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.Error(t, cmd.Run(), stdout.String(), stderr.String())
require.Contains(t, stdout.String(), "Check complete, 2 warnings have been found!")
require.Contains(t, stdout.String(), dockerfilePath+":2")
require.Contains(t, stdout.String(), dockerfilePath+":3")
})
}
func testBuildExtraHosts(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM busybox
RUN cat /etc/hosts | grep myhost | grep 1.2.3.4
RUN cat /etc/hosts | grep myhostmulti | grep 162.242.195.81
RUN cat /etc/hosts | grep myhostmulti | grep 162.242.195.82
`)
dir := tmpdir(t, fstest.CreateFile("Dockerfile", dockerfile, 0o600))
cmd := buildxCmd(sb, withArgs("build",
"--add-host=myhost=1.2.3.4",
"--add-host=myhostmulti=162.242.195.81",
"--add-host=myhostmulti=162.242.195.82",
"--output=type=cacheonly", dir),
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
}
func testBuildIndexAnnotationsLoadDocker(t *testing.T, sb integration.Sandbox) {
if sb.DockerAddress() == "" {
t.Skip("only testing with docker available")
}
skipNoCompatBuildKit(t, sb, ">= 0.11.0-0", "annotations")
dir := createTestProject(t)
out, err := buildCmd(sb, withArgs("--annotation", "index:foo=bar", "--provenance", "false", "--output", "type=docker", dir))
require.Error(t, err, out)
require.Contains(t, out, "index annotations not supported for single platform export")
}
func createTestProject(t *testing.T) string {
dockerfile := []byte(`
FROM busybox:latest AS base
COPY foo /etc/foo
RUN cp /etc/foo /etc/bar
FROM scratch
COPY --from=base /etc/bar /bar
`)
dir := tmpdir(t,
fstest.CreateFile("Dockerfile", dockerfile, 0o600),
fstest.CreateFile("foo", []byte("foo"), 0o600),
)
return dir
}
func createOCILayoutImage(t *testing.T, layoutPath, fileName string, fileContents []byte, tag string) digest.Digest {
t.Helper()
store, err := local.NewStore(layoutPath)
require.NoError(t, err)
layerBytes := bytes.NewBuffer(nil)
tw := tar.NewWriter(layerBytes)
err = tw.WriteHeader(&tar.Header{
Name: fileName,
Mode: 0o644,
Size: int64(len(fileContents)),
})
require.NoError(t, err)
_, err = tw.Write(fileContents)
require.NoError(t, err)
require.NoError(t, tw.Close())
ctx := context.Background()
layerDesc := ocispecs.Descriptor{
MediaType: ocispecs.MediaTypeImageLayer,
Digest: digest.FromBytes(layerBytes.Bytes()),
Size: int64(layerBytes.Len()),
}
err = content.WriteBlob(ctx, store, "layer-"+layerDesc.Digest.String(), bytes.NewReader(layerBytes.Bytes()), layerDesc)
require.NoError(t, err)
cfgBytes, err := json.Marshal(ocispecs.Image{
Platform: ocispecs.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ocispecs.ImageConfig{
WorkingDir: "/",
},
RootFS: ocispecs.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{layerDesc.Digest},
},
})
require.NoError(t, err)
cfgDesc := ocispecs.Descriptor{
MediaType: ocispecs.MediaTypeImageConfig,
Digest: digest.FromBytes(cfgBytes),
Size: int64(len(cfgBytes)),
}
err = content.WriteBlob(ctx, store, "config-"+cfgDesc.Digest.String(), bytes.NewReader(cfgBytes), cfgDesc)
require.NoError(t, err)
manifestBytes, err := json.Marshal(ocispecs.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: ocispecs.MediaTypeImageManifest,
Config: cfgDesc,
Layers: []ocispecs.Descriptor{layerDesc},
})
require.NoError(t, err)
manifestDesc := ocispecs.Descriptor{
MediaType: ocispecs.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestBytes),
Size: int64(len(manifestBytes)),
}
err = content.WriteBlob(ctx, store, "manifest-"+manifestDesc.Digest.String(), bytes.NewReader(manifestBytes), manifestDesc)
require.NoError(t, err)
idx := ociindex.NewStoreIndex(layoutPath)
err = idx.Put(manifestDesc, ociindex.Tag(tag))
require.NoError(t, err)
return manifestDesc.Digest
}