mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
ExecOp returned failed outputs together with ExecError. When the exec was canceled, sharedOp dropped those outputs without releasing them, leaving their cache records in use. Make Op.Exec return nil outputs on error and move failed ExecOp outputs into ExecError. sharedOp now retains clones of a cached error's refs until the vertex is released, so builds sharing a failed vertex can still use its snapshots after another build is discarded. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
1075 lines
27 KiB
Go
1075 lines
27 KiB
Go
package dockerfile
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/snapshots"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
"github.com/containerd/continuity/fs/fstest"
|
|
controlapi "github.com/moby/buildkit/api/services/control"
|
|
cacheimporttypes "github.com/moby/buildkit/cache/remotecache/v1/types"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/frontend/dockerui"
|
|
"github.com/moby/buildkit/util/contentutil"
|
|
"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/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tonistiigi/fsutil"
|
|
)
|
|
|
|
func testCacheReleased(t *testing.T, sb integration.Sandbox) {
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := []byte(integration.UnixOrWindows(
|
|
`
|
|
FROM busybox
|
|
`,
|
|
`
|
|
FROM nanoserver
|
|
`,
|
|
))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
}
|
|
|
|
func testMissingCopySourceReleasesCache(t *testing.T, sb integration.Sandbox) {
|
|
f := getFrontend(t, sb)
|
|
dir := integration.Tmpdir(t,
|
|
fstest.CreateFile("Dockerfile", []byte("FROM scratch\nCOPY missing.txt /\n"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
for range 2 {
|
|
_, err := f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.ErrorContains(t, err, "missing.txt")
|
|
}
|
|
checkAllReleasable(t, c, sb, false)
|
|
}
|
|
|
|
func testCanceledRunReleasesCache(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
f := getFrontend(t, sb)
|
|
dir := integration.Tmpdir(t, fstest.CreateFile("Dockerfile", []byte(`FROM busybox
|
|
ARG BUST
|
|
RUN echo "$BUST" > /parent && dd if=/dev/zero of=/a bs=1M count=1
|
|
RUN dd if=/dev/zero of=/b bs=1M count=1 && echo CANCEL-READY && sleep 120
|
|
`), 0600))
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
for i := range 2 {
|
|
ctx, cancel := context.WithCancelCause(sb.Context())
|
|
ctx, timeoutCancel := context.WithTimeoutCause(ctx, time.Minute, errors.New("RUN did not reach cancellation point"))
|
|
status := make(chan *client.SolveStatus)
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
started := false
|
|
for st := range status {
|
|
for _, log := range st.Logs {
|
|
if strings.Contains(string(log.Data), "CANCEL-READY") {
|
|
started = true
|
|
cancel(errors.WithStack(context.Canceled))
|
|
}
|
|
}
|
|
}
|
|
done <- started
|
|
}()
|
|
_, err := f.Solve(ctx, c, client.SolveOpt{
|
|
FrontendAttrs: map[string]string{"build-arg:BUST": strconv.Itoa(i)},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, status)
|
|
timeoutCancel()
|
|
cancel(errors.WithStack(context.Canceled))
|
|
require.True(t, <-done, "build must be cancelled during RUN")
|
|
require.Error(t, err)
|
|
}
|
|
// Cancellation must release the cache even while build history is retained.
|
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
|
du, err := c.DiskUsage(sb.Context())
|
|
require.NoError(t, err)
|
|
for _, record := range du {
|
|
require.False(t, record.InUse, "cache record %s is still in use", record.ID)
|
|
}
|
|
}, 10*time.Second, 100*time.Millisecond)
|
|
require.NoError(t, c.Prune(sb.Context(), nil, client.PruneAll))
|
|
du, err := c.DiskUsage(sb.Context())
|
|
require.NoError(t, err)
|
|
require.Empty(t, du)
|
|
}
|
|
|
|
func testExportCacheLoop(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheImport, workers.FeatureCacheBackendLocal)
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := []byte(integration.UnixOrWindows(
|
|
`
|
|
FROM alpine as base
|
|
RUN echo aa > /foo
|
|
WORKDIR /bar
|
|
|
|
FROM base as base1
|
|
COPY hello.txt .
|
|
|
|
FROM base as base2
|
|
COPY --from=base1 /bar/hello.txt .
|
|
RUN true
|
|
|
|
FROM scratch
|
|
COPY --from=base2 /foo /f
|
|
`,
|
|
`
|
|
FROM nanoserver AS base
|
|
USER ContainerAdministrator
|
|
RUN echo aa> foo
|
|
WORKDIR /bar
|
|
|
|
FROM base AS base1
|
|
USER ContainerAdministrator
|
|
COPY hello.txt .
|
|
|
|
FROM base AS base2
|
|
USER ContainerAdministrator
|
|
COPY --from=base1 /bar/hello.txt .
|
|
|
|
FROM nanoserver
|
|
COPY --from=base2 foo f
|
|
`,
|
|
))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("hello.txt", []byte("hello"), 0600),
|
|
)
|
|
|
|
cacheDir := t.TempDir()
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
CacheExports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": filepath.Join(cacheDir, "cache"),
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = c.Prune(sb.Context(), nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
CacheExports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": filepath.Join(cacheDir, "cache"),
|
|
},
|
|
},
|
|
},
|
|
CacheImports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"src": filepath.Join(cacheDir, "cache"),
|
|
},
|
|
},
|
|
},
|
|
FrontendAttrs: map[string]string{},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// testCacheMultiPlatformImportExport checks that the build cache works across
|
|
// multiple platforms at once.
|
|
//
|
|
// It builds the same Dockerfile for two platforms, pushes the result to a
|
|
// registry with an inline cache, and then rebuilds from that cache twice. Each
|
|
// rebuild must produce exactly the same image (same digest and same file
|
|
// contents per platform) as the first build, proving the cache was reused
|
|
// instead of re-running the build steps.
|
|
func testCacheMultiPlatformImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureDirectPush,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheBackendInline,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
f := getFrontend(t, sb)
|
|
|
|
// Two-platform matrix per OS. TARGETARCH for linux/arm/v7 is "arm";
|
|
// for windows/arm64 it is "arm64".
|
|
platformAttr := integration.UnixOrWindows("linux/amd64,linux/arm/v7", "windows/amd64,windows/arm64")
|
|
platform1 := integration.UnixOrWindows("linux/amd64", "windows/amd64")
|
|
platform2 := integration.UnixOrWindows("linux/arm/v7", "windows/arm64")
|
|
arch2 := integration.UnixOrWindows("arm", "arm64")
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// Windows equivalent of the Linux RUN: nanoserver lacks PowerShell, sha256sum,
|
|
// and /dev/urandom, so use cmd.exe with TARGETARCH+RANDOM+TIME for uniqueness.
|
|
dockerfile := []byte(integration.UnixOrWindows(
|
|
`
|
|
FROM --platform=$BUILDPLATFORM busybox AS base
|
|
ARG TARGETARCH
|
|
RUN echo -n $TARGETARCH> arch && cat /dev/urandom | head -c 100 | sha256sum > unique
|
|
FROM scratch
|
|
COPY --from=base unique /
|
|
COPY --from=base arch /
|
|
`,
|
|
`
|
|
FROM --platform=$BUILDPLATFORM nanoserver AS base
|
|
USER ContainerAdministrator
|
|
ARG TARGETARCH
|
|
RUN ["cmd","/S","/C","echo %TARGETARCH%>C:\\arch & echo %TARGETARCH%-%RANDOM%-%RANDOM%-%RANDOM%-%TIME%-%DATE%>C:\\unique"]
|
|
FROM nanoserver
|
|
COPY --from=base /unique /
|
|
COPY --from=base /arch /
|
|
`,
|
|
))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
target := registry + "/buildkit/testexportdf:multi"
|
|
|
|
exportCache := []client.CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
}
|
|
importCache := target + "-img"
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"push": "true",
|
|
"name": target + "-img",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: exportCache,
|
|
FrontendAttrs: map[string]string{
|
|
"platform": platformAttr,
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
desc, provider, err := contentutil.ProviderFromRef(sb.Context(), target+"-img")
|
|
require.NoError(t, err)
|
|
|
|
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(imgs.Images))
|
|
|
|
// Windows layers: nanoserver adds 1 base OS layer (shifting indices by +1)
|
|
// and tar entries are nested under "Files/".
|
|
archLayerIdx := integration.UnixOrWindows(1, 2)
|
|
uniqueLayerIdx := integration.UnixOrWindows(0, 1)
|
|
archKey := integration.UnixOrWindows("arch", "Files/arch")
|
|
uniqueKey := integration.UnixOrWindows("unique", "Files/unique")
|
|
|
|
require.Equal(t, "amd64", strings.TrimSpace(string(imgs.Find(platform1).Layers[archLayerIdx][archKey].Data)))
|
|
dtamd := imgs.Find(platform1).Layers[uniqueLayerIdx][uniqueKey].Data
|
|
dtarm := imgs.Find(platform2).Layers[uniqueLayerIdx][uniqueKey].Data
|
|
require.NotEqual(t, dtamd, dtarm)
|
|
|
|
for range 2 {
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
FrontendAttrs: map[string]string{
|
|
"cache-from": importCache,
|
|
"platform": platformAttr,
|
|
},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"push": "true",
|
|
"name": target + "-img",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: exportCache,
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
desc2, provider, err := contentutil.ProviderFromRef(sb.Context(), target+"-img")
|
|
require.NoError(t, err)
|
|
|
|
imgs, err = testutil.ReadImages(sb.Context(), provider, desc2)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(imgs.Images))
|
|
|
|
require.Equal(t, arch2, strings.TrimSpace(string(imgs.Find(platform2).Layers[archLayerIdx][archKey].Data)))
|
|
dtamd2 := imgs.Find(platform1).Layers[uniqueLayerIdx][uniqueKey].Data
|
|
dtarm2 := imgs.Find(platform2).Layers[uniqueLayerIdx][uniqueKey].Data
|
|
require.Equal(t, string(dtamd), string(dtamd2))
|
|
require.Equal(t, string(dtarm), string(dtarm2))
|
|
}
|
|
}
|
|
|
|
func testImageManifestCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
f := getFrontend(t, sb)
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
dockerfile := integration.UnixOrWindows([]byte(`
|
|
FROM busybox AS base
|
|
COPY foo const
|
|
#RUN echo -n foobar > const
|
|
RUN cat /dev/urandom | head -c 100 | sha256sum > unique
|
|
FROM scratch
|
|
COPY --from=base const /
|
|
COPY --from=base unique /
|
|
`), []byte(`
|
|
FROM nanoserver:latest AS base
|
|
USER ContainerAdministrator
|
|
COPY foo const
|
|
# RUN echo foobar > const
|
|
RUN echo %RANDOM%%RANDOM%> unique
|
|
FROM nanoserver:latest
|
|
COPY --from=base const /
|
|
COPY --from=base unique /
|
|
`))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("foo", []byte("foobar"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
destDir := t.TempDir()
|
|
|
|
target := registry + "/buildkit/testexportdf:latest"
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
CacheExports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
"oci-mediatypes": "true",
|
|
},
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
desc, provider, err := contentutil.ProviderFromRef(sb.Context(), target)
|
|
require.NoError(t, err)
|
|
img, err := testutil.ReadImage(sb.Context(), provider, desc)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, ocispecs.MediaTypeImageManifest, img.Manifest.MediaType)
|
|
require.Equal(t, cacheimporttypes.CacheConfigMediaTypeV0, img.Manifest.Config.MediaType)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "foobar", string(dt))
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
destDir = t.TempDir()
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
FrontendAttrs: map[string]string{
|
|
"cache-from": target,
|
|
},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt2, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "foobar", string(dt2))
|
|
|
|
dt2, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, string(dt), string(dt2))
|
|
}
|
|
|
|
func testCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
f := getFrontend(t, sb)
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
dockerfile := integration.UnixOrWindows([]byte(`
|
|
FROM busybox AS base
|
|
COPY foo const
|
|
#RUN echo -n foobar > const
|
|
RUN cat /dev/urandom | head -c 100 | sha256sum > unique
|
|
FROM scratch
|
|
COPY --from=base const /
|
|
COPY --from=base unique /
|
|
`), []byte(`
|
|
FROM nanoserver:latest AS base
|
|
USER ContainerAdministrator
|
|
COPY foo const
|
|
# RUN echo foobar > const
|
|
RUN echo %RANDOM%%RANDOM%> unique
|
|
FROM nanoserver:latest
|
|
COPY --from=base const /
|
|
COPY --from=base unique /
|
|
`))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("foo", []byte("foobar"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
destDir := t.TempDir()
|
|
|
|
target := registry + "/buildkit/testexportdf:latest"
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
CacheExports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{"ref": target},
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "foobar", string(dt))
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
destDir = t.TempDir()
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
FrontendAttrs: map[string]string{
|
|
"cache-from": target,
|
|
},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt2, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "foobar", string(dt2))
|
|
|
|
dt2, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, string(dt), string(dt2))
|
|
}
|
|
|
|
func testReproducibleIDs(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := []byte(integration.UnixOrWindows(
|
|
`
|
|
FROM busybox
|
|
ENV foo=bar
|
|
COPY foo /
|
|
RUN echo bar > bar
|
|
`,
|
|
`
|
|
FROM nanoserver
|
|
USER ContainerAdministrator
|
|
ENV foo=bar
|
|
COPY foo /
|
|
RUN echo bar > bar
|
|
`,
|
|
))
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("foo", []byte("foo-contents"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
target := "example.com/moby/dockerfileids:test"
|
|
opt := client.SolveOpt{
|
|
FrontendAttrs: map[string]string{},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
},
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
target2 := "example.com/moby/dockerfileids2:test"
|
|
opt.Exports[0].Attrs["name"] = target2
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("rest of test requires containerd worker")
|
|
}
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
img, err := client.ImageService().Get(ctx, target)
|
|
require.NoError(t, err)
|
|
img2, err := client.ImageService().Get(ctx, target2)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, img.Target, img2.Target)
|
|
}
|
|
|
|
func testImportExportReproducibleIDs(t *testing.T, sb integration.Sandbox) {
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("test requires containerd worker")
|
|
}
|
|
|
|
f := getFrontend(t, sb)
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
dockerfile := integration.UnixOrWindows([]byte(`
|
|
FROM busybox
|
|
ENV foo=bar
|
|
COPY foo /
|
|
RUN echo bar > bar
|
|
`), []byte(`
|
|
FROM nanoserver:latest
|
|
USER ContainerAdministrator
|
|
ENV foo=bar
|
|
COPY foo /
|
|
RUN echo bar> bar
|
|
`))
|
|
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("foo", []byte("foobar"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
target := "example.com/moby/dockerfileexpids:test"
|
|
cacheTarget := registry + "/test/dockerfileexpids:cache"
|
|
opt := client.SolveOpt{
|
|
FrontendAttrs: map[string]string{},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []client.CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{"ref": cacheTarget},
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}
|
|
|
|
ctd, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer ctd.Close()
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
img, err := ctd.ImageService().Get(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
err = ctd.ImageService().Delete(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
target2 := "example.com/moby/dockerfileexpids2:test"
|
|
|
|
opt.Exports[0].Attrs["name"] = target2
|
|
opt.FrontendAttrs["cache-from"] = cacheTarget
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
img2, err := ctd.ImageService().Get(ctx, target2)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, img.Target, img2.Target)
|
|
}
|
|
|
|
func testNoCache(t *testing.T, sb integration.Sandbox) {
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := integration.UnixOrWindows([]byte(`
|
|
FROM busybox AS s0
|
|
RUN cat /dev/urandom | head -c 100 | sha256sum | tee unique
|
|
FROM busybox AS s1
|
|
RUN cat /dev/urandom | head -c 100 | sha256sum | tee unique2
|
|
FROM scratch
|
|
COPY --from=s0 unique /
|
|
COPY --from=s1 unique2 /
|
|
`), []byte(`
|
|
FROM nanoserver:latest AS s0
|
|
USER ContainerAdministrator
|
|
RUN echo %RANDOM%%RANDOM%%RANDOM%> unique
|
|
FROM nanoserver:latest AS s1
|
|
USER ContainerAdministrator
|
|
RUN echo %RANDOM%%RANDOM%%RANDOM%> unique2
|
|
FROM nanoserver:latest
|
|
COPY --from=s0 unique /
|
|
COPY --from=s1 unique2 /
|
|
`))
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
destDir := t.TempDir()
|
|
|
|
opt := client.SolveOpt{
|
|
FrontendAttrs: map[string]string{},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
destDir2 := t.TempDir()
|
|
|
|
opt.FrontendAttrs["no-cache"] = ""
|
|
opt.Exports[0].OutputDir = destDir2
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
unique1Dir1, err := os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
unique1Dir2, err := os.ReadFile(filepath.Join(destDir2, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
unique2Dir1, err := os.ReadFile(filepath.Join(destDir, "unique2"))
|
|
require.NoError(t, err)
|
|
|
|
unique2Dir2, err := os.ReadFile(filepath.Join(destDir2, "unique2"))
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, string(unique1Dir1), string(unique1Dir2))
|
|
require.NotEqual(t, string(unique2Dir1), string(unique2Dir2))
|
|
|
|
destDir3 := t.TempDir()
|
|
|
|
opt.FrontendAttrs["no-cache"] = "s1"
|
|
opt.Exports[0].OutputDir = destDir3
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
unique1Dir3, err := os.ReadFile(filepath.Join(destDir3, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
unique2Dir3, err := os.ReadFile(filepath.Join(destDir3, "unique2"))
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, string(unique1Dir2), string(unique1Dir3))
|
|
require.NotEqual(t, string(unique2Dir1), string(unique2Dir3))
|
|
}
|
|
|
|
// moby/buildkit#5305
|
|
func testCacheMountModeNoCache(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := []byte(`
|
|
FROM busybox AS base
|
|
ARG FOO=abc
|
|
RUN --mount=type=cache,target=/cache,mode=0773 touch /cache/$FOO && ls -l /cache | wc -l > /out
|
|
|
|
FROM scratch
|
|
COPY --from=base /out /
|
|
`)
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
destDir := t.TempDir()
|
|
|
|
opt := client.SolveOpt{
|
|
FrontendAttrs: map[string]string{},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
opt.FrontendAttrs["no-cache"] = ""
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "out"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "2\n", string(dt))
|
|
|
|
opt.FrontendAttrs["build-arg:FOO"] = "def"
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "out"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "2\n", string(dt))
|
|
|
|
// safety check without no-cache
|
|
delete(opt.FrontendAttrs, "no-cache")
|
|
opt.FrontendAttrs["build-arg:FOO"] = "ghi"
|
|
|
|
_, err = f.Solve(sb.Context(), c, opt, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "out"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "3\n", string(dt))
|
|
}
|
|
|
|
// #2008
|
|
func testWildcardRenameCache(t *testing.T, sb integration.Sandbox) {
|
|
f := getFrontend(t, sb)
|
|
|
|
dockerfile := []byte(integration.UnixOrWindows(
|
|
`
|
|
FROM alpine
|
|
COPY file* /files/
|
|
RUN ls /files/file1
|
|
`,
|
|
`
|
|
FROM nanoserver
|
|
COPY file* /
|
|
RUN dir file1
|
|
`,
|
|
))
|
|
dir := integration.Tmpdir(
|
|
t,
|
|
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
|
fstest.CreateFile("file1", []byte("foo"), 0600),
|
|
)
|
|
|
|
c, err := client.New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = os.Rename(filepath.Join(dir.Name, "file1"), filepath.Join(dir.Name, "file2"))
|
|
require.NoError(t, err)
|
|
|
|
// cache should be invalidated and build should fail
|
|
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
dockerui.DefaultLocalNameDockerfile: dir,
|
|
dockerui.DefaultLocalNameContext: dir,
|
|
},
|
|
}, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func checkAllReleasable(t *testing.T, c *client.Client, sb integration.Sandbox, checkContent bool) {
|
|
cl, err := c.ControlClient().ListenBuildHistory(sb.Context(), &controlapi.BuildHistoryRequest{
|
|
EarlyExit: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for {
|
|
resp, err := cl.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
_, err = c.ControlClient().UpdateBuildHistory(sb.Context(), &controlapi.UpdateBuildHistoryRequest{
|
|
Ref: resp.Record.Ref,
|
|
Delete: true,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
retries := 0
|
|
loop0:
|
|
for {
|
|
require.Less(t, retries, 20)
|
|
retries++
|
|
du, err := c.DiskUsage(sb.Context())
|
|
require.NoError(t, err)
|
|
for _, d := range du {
|
|
if d.InUse {
|
|
time.Sleep(500 * time.Millisecond)
|
|
continue loop0
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
err = c.Prune(sb.Context(), nil, client.PruneAll)
|
|
require.NoError(t, err)
|
|
|
|
du, err := c.DiskUsage(sb.Context())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(du))
|
|
|
|
// examine contents of exported tars (requires containerd)
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
return
|
|
}
|
|
|
|
// TODO: make public pull helper function so this can be checked for standalone as well
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
// pick default snapshotter on Windows, hence ""
|
|
snapshotName := integration.UnixOrWindows("overlayfs", "")
|
|
snapshotService := client.SnapshotService(snapshotName)
|
|
|
|
retries = 0
|
|
for {
|
|
count := 0
|
|
err = snapshotService.Walk(ctx, func(context.Context, snapshots.Info) error {
|
|
count++
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
if count == 0 {
|
|
break
|
|
}
|
|
require.Less(t, retries, 20)
|
|
retries++
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
if !checkContent {
|
|
return
|
|
}
|
|
|
|
retries = 0
|
|
for {
|
|
count := 0
|
|
err = client.ContentStore().Walk(ctx, func(content.Info) error {
|
|
count++
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
if count == 0 {
|
|
break
|
|
}
|
|
require.Less(t, retries, 20)
|
|
retries++
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|