Files
buildkit/frontend/dockerfile/dockerfile_export_test.go
Tonis Tiigi 2939b5b2fe dockerfile: split integration tests
Move Dockerfile integration tests into topic-specific files while preserving
registration order and test bodies.

Keep shared helpers centralized where they are used across topics, and move
topic-local helpers with their callers. Fold small one-off buckets into the
broader ADD/COPY groupings and keep same-file-only registration lists next to
their tests.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-07-07 17:31:30 -07:00

400 lines
10 KiB
Go

package dockerfile
import (
"archive/tar"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/containerd/continuity/fs/fstest"
"github.com/containerd/platforms"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/frontend/dockerui"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/upload/uploadprovider"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/testutil"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
"github.com/tonistiigi/fsutil"
)
func testTarExporterBasic(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
dockerfile := []byte(integration.UnixOrWindows(
`
FROM scratch
COPY foo foo
`,
`
FROM nanoserver
COPY foo foo
`,
))
dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("data"), 0600),
)
c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
buf := &bytes.Buffer{}
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterTar,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: buf}),
},
},
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
}, nil)
require.NoError(t, err)
m, err := testutil.ReadTarToMap(buf.Bytes(), false)
require.NoError(t, err)
mi, ok := m["foo"]
require.Equal(t, true, ok)
require.Equal(t, "data", string(mi.Data))
}
func testTarExporterMulti(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows", "Multi-platform tar export test specifically targets linux/amd64 and darwin/amd64 platforms as feature it's supported on Windows")
f := getFrontend(t, sb)
dockerfile := []byte(`
FROM scratch AS stage-linux
COPY foo forlinux
FROM scratch AS stage-darwin
COPY bar fordarwin
FROM stage-$TARGETOS
`)
dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("data"), 0600),
fstest.CreateFile("bar", []byte("data2"), 0600),
)
c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
buf := &bytes.Buffer{}
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterTar,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: buf}),
},
},
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
}, nil)
require.NoError(t, err)
m, err := testutil.ReadTarToMap(buf.Bytes(), false)
require.NoError(t, err)
mi, ok := m["forlinux"]
require.Equal(t, true, ok)
require.Equal(t, "data", string(mi.Data))
// repeat multi-platform
buf = &bytes.Buffer{}
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
Exports: []client.ExportEntry{
{
Type: client.ExporterTar,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: buf}),
},
},
FrontendAttrs: map[string]string{
"platform": "linux/amd64,darwin/amd64",
},
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
}, nil)
require.NoError(t, err)
m, err = testutil.ReadTarToMap(buf.Bytes(), false)
require.NoError(t, err)
mi, ok = m["linux_amd64/forlinux"]
require.Equal(t, true, ok)
require.Equal(t, "data", string(mi.Data))
mi, ok = m["darwin_amd64/fordarwin"]
require.Equal(t, true, ok)
require.Equal(t, "data2", string(mi.Data))
}
func testExportMultiPlatform(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows", "Multi-platform builds from scratch fail on Windows: 'number of mounts should always be 1 for Windows layers'")
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
f := getFrontend(t, sb)
dockerfile := []byte(`
FROM scratch
ARG TARGETARCH
ARG TARGETPLATFORM
LABEL target=$TARGETPLATFORM
COPY arch-$TARGETARCH whoami
`)
dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("arch-arm", []byte(`i am arm`), 0600),
fstest.CreateFile("arch-amd64", []byte(`i am amd64`), 0600),
fstest.CreateFile("arch-s390x", []byte(`i am s390x`), 0600),
fstest.CreateFile("arch-ppc64le", []byte(`i am ppc64le`), 0600),
)
c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
destDir := t.TempDir()
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
FrontendAttrs: map[string]string{
"platform": "windows/amd64,linux/arm,linux/s390x",
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "windows_amd64/whoami"))
require.NoError(t, err)
require.Equal(t, "i am amd64", string(dt))
dt, err = os.ReadFile(filepath.Join(destDir, "linux_arm_v7/whoami"))
require.NoError(t, err)
require.Equal(t, "i am arm", string(dt))
dt, err = os.ReadFile(filepath.Join(destDir, "linux_s390x/whoami"))
require.NoError(t, err)
require.Equal(t, "i am s390x", string(dt))
// repeat with oci exporter
destDir = t.TempDir()
out := filepath.Join(destDir, "out.tar")
outW, err := os.Create(out)
require.NoError(t, err)
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
FrontendAttrs: map[string]string{
"platform": "windows/amd64,linux/arm/v6,linux/ppc64le",
},
Exports: []client.ExportEntry{
{
Type: client.ExporterOCI,
Output: fixedWriteCloser(outW),
},
},
}, nil)
require.NoError(t, err)
dt, err = os.ReadFile(filepath.Join(destDir, "out.tar"))
require.NoError(t, err)
m, err := testutil.ReadTarToMap(dt, false)
require.NoError(t, err)
var idx ocispecs.Index
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &idx)
require.NoError(t, err)
mlistHex := idx.Manifests[0].Digest.Hex()
idx = ocispecs.Index{}
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mlistHex].Data, &idx)
require.NoError(t, err)
require.Equal(t, 3, len(idx.Manifests))
for i, exp := range []struct {
p string
os string
arch string
dt string
}{
{p: "windows/amd64", os: "windows", arch: "amd64", dt: "i am amd64"},
{p: "linux/arm/v6", os: "linux", arch: "arm", dt: "i am arm"},
{p: "linux/ppc64le", os: "linux", arch: "ppc64le", dt: "i am ppc64le"},
} {
t.Run(exp.p, func(t *testing.T) {
require.Equal(t, exp.p, platforms.Format(*idx.Manifests[i].Platform))
var mfst ocispecs.Manifest
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+idx.Manifests[i].Digest.Hex()].Data, &mfst)
require.NoError(t, err)
require.Equal(t, 1, len(mfst.Layers))
m2, err := testutil.ReadTarToMap(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[0].Digest.Hex()].Data, true)
require.NoError(t, err)
require.Equal(t, exp.dt, string(m2["whoami"].Data))
var img ocispecs.Image
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Config.Digest.Hex()].Data, &img)
require.NoError(t, err)
require.Equal(t, exp.os, img.OS)
require.Equal(t, exp.arch, img.Architecture)
v, ok := img.Config.Labels["target"]
require.True(t, ok)
require.Equal(t, exp.p, v)
})
}
}
func testTarContext(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
imgName := integration.UnixOrWindows("scratch", "nanoserver")
dockerfile := fmt.Appendf(nil, `
FROM %s
COPY foo /`, imgName)
foo := []byte("contents")
buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf)
err := tw.WriteHeader(&tar.Header{
Name: "Dockerfile",
Typeflag: tar.TypeReg,
Size: int64(len(dockerfile)),
Mode: 0644,
})
require.NoError(t, err)
_, err = tw.Write(dockerfile)
require.NoError(t, err)
err = tw.WriteHeader(&tar.Header{
Name: "foo",
Typeflag: tar.TypeReg,
Size: int64(len(foo)),
Mode: 0644,
})
require.NoError(t, err)
_, err = tw.Write(foo)
require.NoError(t, err)
err = tw.Close()
require.NoError(t, err)
c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
up := uploadprovider.New()
url := up.Add(io.NopCloser(buf))
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
FrontendAttrs: map[string]string{
"context": url,
},
Session: []session.Attachable{up},
}, nil)
require.NoError(t, err)
}
func testTarContextExternalDockerfile(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
foo := []byte("contents")
buf := bytes.NewBuffer(nil)
tw := tar.NewWriter(buf)
err := tw.WriteHeader(&tar.Header{
Name: "sub/dir/foo",
Typeflag: tar.TypeReg,
Size: int64(len(foo)),
Mode: 0644,
})
require.NoError(t, err)
_, err = tw.Write(foo)
require.NoError(t, err)
err = tw.Close()
require.NoError(t, err)
imgName := integration.UnixOrWindows("scratch", "nanoserver")
dockerfile := fmt.Appendf(nil, `
FROM %s
COPY foo bar
`, imgName)
dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
)
c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
up := uploadprovider.New()
url := up.Add(io.NopCloser(buf))
// repeat with changed default args should match the old cache
destDir := t.TempDir()
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
FrontendAttrs: map[string]string{
"context": url,
"dockerfilekey": dockerui.DefaultLocalNameDockerfile,
"contextsubdir": "sub/dir",
},
Session: []session.Attachable{up},
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "bar"))
require.NoError(t, err)
require.Equal(t, "contents", string(dt))
}