mirror of
https://github.com/moby/buildkit.git
synced 2026-08-05 07:10:23 +00:00
864 lines
24 KiB
Go
864 lines
24 KiB
Go
package client
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/platforms"
|
|
controlapi "github.com/moby/buildkit/api/services/control"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
gatewaypb "github.com/moby/buildkit/frontend/gateway/pb"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/session/exporter"
|
|
"github.com/moby/buildkit/session/exporter/exporterprovider"
|
|
"github.com/moby/buildkit/session/filesync"
|
|
"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/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func testExportLocalForcePlatformSplit(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
st := llb.Scratch().File(
|
|
llb.Mkfile("foo", 0600, []byte("hello")),
|
|
)
|
|
|
|
def, err := st.Marshal(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
Attrs: map[string]string{
|
|
"platform-split": "true",
|
|
},
|
|
},
|
|
},
|
|
}, "", frontend, nil)
|
|
require.NoError(t, err)
|
|
|
|
fis, err := os.ReadDir(destDir)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, fis, 1, "expected one files in the output directory")
|
|
|
|
expPlatform := strings.ReplaceAll(platforms.FormatAll(platforms.Normalize(platforms.DefaultSpec())), "/", "_")
|
|
_, err = os.Stat(filepath.Join(destDir, expPlatform+"/"))
|
|
require.NoError(t, err)
|
|
|
|
fis, err = os.ReadDir(filepath.Join(destDir, expPlatform))
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, fis, 1, "expected one files in the output directory for platform "+expPlatform)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, expPlatform, "foo"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "hello", string(dt))
|
|
}
|
|
|
|
func testExportLocalModeCopyKeepsStaleDestinationFiles(t *testing.T, sb integration.Sandbox) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
st := llb.Scratch().File(
|
|
llb.Mkfile("fresh.txt", 0600, []byte("fresh")),
|
|
)
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
err = os.WriteFile(filepath.Join(destDir, "stale.txt"), []byte("stale"), 0600)
|
|
require.NoError(t, err)
|
|
err = os.MkdirAll(filepath.Join(destDir, "stale-dir"), 0755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(destDir, "stale-dir", "old.txt"), []byte("stale"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "fresh.txt"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "fresh", string(dt))
|
|
|
|
_, err = os.Stat(filepath.Join(destDir, "stale.txt"))
|
|
require.NoError(t, err)
|
|
_, err = os.Stat(filepath.Join(destDir, "stale-dir", "old.txt"))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testExportLocalModeCopyMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox) {
|
|
testExportLocalModeMultiPlatformKeepsAllPlatforms(t, sb, false)
|
|
}
|
|
|
|
func testExportLocalModeDeleteRemovesStaleDestinationFiles(t *testing.T, sb integration.Sandbox) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
st := llb.Scratch().File(
|
|
llb.Mkfile("fresh.txt", 0600, []byte("fresh")),
|
|
)
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
err = os.WriteFile(filepath.Join(destDir, "stale.txt"), []byte("stale"), 0600)
|
|
require.NoError(t, err)
|
|
err = os.MkdirAll(filepath.Join(destDir, "stale-dir"), 0755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(destDir, "stale-dir", "old.txt"), []byte("stale"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
Attrs: map[string]string{
|
|
"mode": "delete",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "fresh.txt"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "fresh", string(dt))
|
|
|
|
_, err = os.Stat(filepath.Join(destDir, "stale.txt"))
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
_, err = os.Stat(filepath.Join(destDir, "stale-dir"))
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
}
|
|
|
|
func testExportLocalModeDeleteMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox) {
|
|
testExportLocalModeMultiPlatformKeepsAllPlatforms(t, sb, true)
|
|
}
|
|
|
|
func testExportLocalModeMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox, deleteMode bool) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
const filesPerPlatform = 20
|
|
platformsToTest := []string{"linux/amd64", "linux/arm64", "linux/arm/v7", "linux/s390x"}
|
|
|
|
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
res := gateway.NewResult()
|
|
expPlatforms := &exptypes.Platforms{
|
|
Platforms: make([]exptypes.Platform, len(platformsToTest)),
|
|
}
|
|
for i, platform := range platformsToTest {
|
|
st := llb.Scratch()
|
|
for j := range filesPerPlatform {
|
|
st = st.File(
|
|
llb.Mkfile(fmt.Sprintf("file-%03d.txt", j), 0600, fmt.Appendf(nil, "%s-%d", platform, j)),
|
|
)
|
|
}
|
|
|
|
def, err := st.Marshal(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ref, err := r.SingleRef()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = ref.ToState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddRef(platform, ref)
|
|
|
|
expPlatforms.Platforms[i] = exptypes.Platform{
|
|
ID: platform,
|
|
Platform: platforms.MustParse(platform),
|
|
}
|
|
}
|
|
dt, err := json.Marshal(expPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
|
|
// Pre-populate dest with directories matching the platform-split naming
|
|
// convention. Delete mode must remove stale files from all platform
|
|
// directories while preserving every platform's fresh output.
|
|
err = os.WriteFile(filepath.Join(destDir, "stale.txt"), []byte("stale"), 0600)
|
|
require.NoError(t, err)
|
|
for _, platform := range platformsToTest {
|
|
platDir := filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_"))
|
|
err = os.MkdirAll(platDir, 0755)
|
|
require.NoError(t, err)
|
|
for j := range filesPerPlatform {
|
|
err = os.WriteFile(filepath.Join(platDir, fmt.Sprintf("old-%03d.txt", j)), []byte("old"), 0600)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
attrs := map[string]string{}
|
|
if deleteMode {
|
|
attrs["mode"] = "delete"
|
|
}
|
|
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
Attrs: attrs,
|
|
},
|
|
},
|
|
}, "", frontend, nil)
|
|
require.NoError(t, err)
|
|
|
|
if deleteMode {
|
|
// Stale top-level file must be gone.
|
|
_, err = os.Stat(filepath.Join(destDir, "stale.txt"))
|
|
require.ErrorIs(t, err, os.ErrNotExist)
|
|
}
|
|
|
|
// Every platform's build output must survive (no cross-platform deletion).
|
|
for _, platform := range platformsToTest {
|
|
platformDir := filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_"))
|
|
for j := range filesPerPlatform {
|
|
dt, err := os.ReadFile(filepath.Join(platformDir, fmt.Sprintf("file-%03d.txt", j)))
|
|
require.NoError(t, err, "missing build output file-%03d.txt for %s", j, platform)
|
|
require.Equal(t, fmt.Sprintf("%s-%d", platform, j), string(dt))
|
|
}
|
|
if deleteMode {
|
|
// Pre-existing files within each platform dir must be cleaned up.
|
|
_, err = os.Stat(filepath.Join(platformDir, "old-000.txt"))
|
|
require.ErrorIs(t, err, os.ErrNotExist, "stale file not removed for %s", platform)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testExportLocalNoPlatformSplit(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
platformsToTest := []string{"linux/amd64", "linux/arm64"}
|
|
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
res := gateway.NewResult()
|
|
expPlatforms := &exptypes.Platforms{
|
|
Platforms: make([]exptypes.Platform, len(platformsToTest)),
|
|
}
|
|
for i, platform := range platformsToTest {
|
|
st := llb.Scratch().File(
|
|
llb.Mkfile("hello-"+strings.ReplaceAll(platform, "/", "-"), 0600, []byte(platform)),
|
|
)
|
|
|
|
def, err := st.Marshal(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ref, err := r.SingleRef()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = ref.ToState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddRef(platform, ref)
|
|
|
|
expPlatforms.Platforms[i] = exptypes.Platform{
|
|
ID: platform,
|
|
Platform: platforms.MustParse(platform),
|
|
}
|
|
}
|
|
dt, err := json.Marshal(expPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
Attrs: map[string]string{
|
|
"platform-split": "false",
|
|
},
|
|
},
|
|
},
|
|
}, "", frontend, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "hello-linux-amd64"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "linux/amd64", string(dt))
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "hello-linux-arm64"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "linux/arm64", string(dt))
|
|
}
|
|
|
|
func testExportLocalNoPlatformSplitOverwrite(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
platformsToTest := []string{"linux/amd64", "linux/arm64"}
|
|
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
res := gateway.NewResult()
|
|
expPlatforms := &exptypes.Platforms{
|
|
Platforms: make([]exptypes.Platform, len(platformsToTest)),
|
|
}
|
|
for i, platform := range platformsToTest {
|
|
st := llb.Scratch().File(
|
|
llb.Mkfile("hello-linux", 0600, []byte(platform)),
|
|
)
|
|
|
|
def, err := st.Marshal(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ref, err := r.SingleRef()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = ref.ToState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddRef(platform, ref)
|
|
|
|
expPlatforms.Platforms[i] = exptypes.Platform{
|
|
ID: platform,
|
|
Platform: platforms.MustParse(platform),
|
|
}
|
|
}
|
|
dt, err := json.Marshal(expPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
Attrs: map[string]string{
|
|
"platform-split": "false",
|
|
},
|
|
},
|
|
},
|
|
}, "", frontend, nil)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "cannot overwrite hello-linux from")
|
|
require.ErrorContains(t, err, "when split option is disabled")
|
|
}
|
|
|
|
func testExporterTargetExists(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
st := llb.Image(imgName)
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
var mdDgst string
|
|
res, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI,
|
|
Attrs: map[string]string{},
|
|
Output: func(m map[string]string) (io.WriteCloser, error) {
|
|
mdDgst = m[exptypes.ExporterImageDigestKey]
|
|
return nil, nil
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
dgst := res.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
|
|
require.True(t, strings.HasPrefix(dgst, "sha256:"))
|
|
require.Equal(t, dgst, mdDgst)
|
|
|
|
require.True(t, strings.HasPrefix(res.ExporterResponse[exptypes.ExporterImageConfigDigestKey], "sha256:"))
|
|
}
|
|
|
|
func testMultipleExporters(t *testing.T, sb integration.Sandbox) {
|
|
requiresLinux(t)
|
|
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
def, err := llb.Scratch().File(llb.Mkfile("foo.txt", 0o755, nil)).Marshal(context.TODO())
|
|
require.NoError(t, err)
|
|
|
|
destDir, destDir2 := t.TempDir(), t.TempDir()
|
|
out := filepath.Join(destDir, "out.tar")
|
|
outW, err := os.Create(out)
|
|
require.NoError(t, err)
|
|
defer outW.Close()
|
|
|
|
out2 := filepath.Join(destDir, "out2.tar")
|
|
outW2, err := os.Create(out2)
|
|
require.NoError(t, err)
|
|
defer outW2.Close()
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
target1, target2 := registry+"/buildkit/build/exporter:image",
|
|
registry+"/buildkit/build/alternative:image"
|
|
|
|
var exporters []ExportEntry
|
|
if workers.IsTestDockerd() {
|
|
exporters = append(exporters, ExportEntry{
|
|
Type: "moby",
|
|
Attrs: map[string]string{
|
|
"name": strings.Join([]string{target1, target2}, ","),
|
|
},
|
|
})
|
|
} else {
|
|
exporters = append(exporters, []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target1,
|
|
},
|
|
},
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target2,
|
|
"oci-mediatypes": "false",
|
|
},
|
|
},
|
|
}...)
|
|
}
|
|
|
|
exporters = append(exporters, []ExportEntry{
|
|
// Ensure that multiple local exporter destinations are written properly
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir2,
|
|
},
|
|
// Ensure that multiple instances of the same exporter are possible
|
|
{
|
|
Type: ExporterTar,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
{
|
|
Type: ExporterTar,
|
|
Output: fixedWriteCloser(outW2),
|
|
},
|
|
}...)
|
|
|
|
ref := identity.NewID()
|
|
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Ref: ref,
|
|
Exports: exporters,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
if workers.IsTestDockerd() {
|
|
require.Equal(t, target1+","+target2, resp.ExporterResponse[exptypes.ExporterImageNameKey])
|
|
} else {
|
|
require.Equal(t, target2, resp.ExporterResponse[exptypes.ExporterImageNameKey])
|
|
}
|
|
require.FileExists(t, filepath.Join(destDir, "out.tar"))
|
|
require.FileExists(t, filepath.Join(destDir, "out2.tar"))
|
|
require.FileExists(t, filepath.Join(destDir, "foo.txt"))
|
|
require.FileExists(t, filepath.Join(destDir2, "foo.txt"))
|
|
|
|
history, err := c.ControlClient().ListenBuildHistory(sb.Context(), &controlapi.BuildHistoryRequest{
|
|
Ref: ref,
|
|
EarlyExit: true,
|
|
})
|
|
require.NoError(t, err)
|
|
for {
|
|
ev, err := history.Recv()
|
|
if err != nil {
|
|
require.Equal(t, io.EOF, err)
|
|
break
|
|
}
|
|
require.Equal(t, ref, ev.Record.Ref)
|
|
|
|
if workers.IsTestDockerd() {
|
|
require.Len(t, ev.Record.Result.Results, 1)
|
|
require.Len(t, ev.Record.Exporters, 5)
|
|
if workers.IsTestDockerdMoby(sb) {
|
|
require.Equal(t, ocispecs.MediaTypeImageConfig, ev.Record.Result.Results[0].MediaType)
|
|
} else {
|
|
require.Equal(t, ocispecs.MediaTypeImageManifest, ev.Record.Result.Results[0].MediaType)
|
|
}
|
|
} else {
|
|
require.Len(t, ev.Record.Result.Results, 2)
|
|
require.Len(t, ev.Record.Exporters, 6)
|
|
require.Equal(t, ocispecs.MediaTypeImageManifest, ev.Record.Result.Results[0].MediaType)
|
|
require.Equal(t, images.MediaTypeDockerSchema2Manifest, ev.Record.Result.Results[1].MediaType)
|
|
}
|
|
require.Equal(t, ev.Record.Result.Results[0], ev.Record.Result.ResultDeprecated)
|
|
}
|
|
}
|
|
|
|
// TODO: Re-enable the skipped test on Windows (see issue #6296)
|
|
func testSessionExporter(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows", "This test passed locally on windows, but failed on github action")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
|
|
c, err := New(context.TODO(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// make an image that is exported there
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
st := integration.UnixOrWindows(
|
|
llb.Scratch(),
|
|
llb.Image(imgName),
|
|
)
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
cmdPrefix := integration.UnixOrWindows(
|
|
`sh -c "echo -n`,
|
|
`cmd /C "echo`,
|
|
)
|
|
run(fmt.Sprintf(`%s first > foo"`, cmdPrefix))
|
|
run(fmt.Sprintf(`%s second > bar"`, cmdPrefix))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
var attachable []session.Attachable
|
|
target := filesync.NewFSSyncTarget()
|
|
|
|
attachable = append(attachable, target)
|
|
|
|
buildID := identity.NewID()
|
|
|
|
outW := bytes.NewBuffer(nil)
|
|
exporterCalled := false
|
|
// The provider has no finalization callback; FinalizeExport returning
|
|
// Unimplemented must not fail the solve.
|
|
exporter := exporterprovider.New(func(ctx context.Context, md map[string][]byte, refs []string) ([]*exporter.ExporterRequest, error) {
|
|
require.Len(t, refs, 1)
|
|
g := c.GatewayClientForBuild(buildID)
|
|
resp, err := g.ReadDir(sb.Context(), &gatewaypb.ReadDirRequest{
|
|
Ref: refs[0],
|
|
DirPath: "/",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.Logf("readdir: %v", resp)
|
|
|
|
require.Equal(t, 2, len(resp.Entries))
|
|
require.Equal(t, "bar", resp.Entries[0].Path)
|
|
require.Equal(t, "foo", resp.Entries[1].Path)
|
|
|
|
exporterCalled = true
|
|
target.Add(filesync.WithFSSync(0, fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW})))
|
|
return []*exporter.ExporterRequest{
|
|
{
|
|
Type: ExporterOCI,
|
|
Attrs: map[string]string{
|
|
"compression": "zstd",
|
|
},
|
|
},
|
|
}, nil
|
|
})
|
|
require.NoError(t, err)
|
|
attachable = append(attachable, exporter)
|
|
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
EnableSessionExporter: true,
|
|
Session: attachable,
|
|
Ref: buildID,
|
|
}, "", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
return c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, exporterCalled)
|
|
|
|
// extract the tar stream to the directory as OCI layout
|
|
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
|
|
require.NoError(t, err)
|
|
|
|
var index ocispecs.Index
|
|
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(index.Manifests))
|
|
digest := index.Manifests[0].Digest
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(m[path.Join("blobs/sha256", digest.Hex())].Data, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
if runtime.GOOS == "windows" {
|
|
mfst.Layers = mfst.Layers[1:] // skip base layer
|
|
}
|
|
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
for _, layer := range mfst.Layers {
|
|
require.Contains(t, layer.MediaType, ocispecs.MediaTypeImageLayerZstd)
|
|
}
|
|
}
|
|
|
|
func testSessionExporterFinalizeExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
st := llb.Image(integration.UnixOrWindows("busybox:latest", "nanoserver:latest"))
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
build := func(name string, p session.Attachable) error {
|
|
_, err := c.Build(sb.Context(), SolveOpt{
|
|
EnableSessionExporter: true,
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": "session-exporter-finalize-" + name,
|
|
},
|
|
},
|
|
},
|
|
Session: []session.Attachable{p},
|
|
Ref: identity.NewID(),
|
|
}, "", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
return c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
}, nil)
|
|
return err
|
|
}
|
|
|
|
callbackErr := errors.New("finalize callback failed")
|
|
for _, tc := range []struct {
|
|
name string
|
|
callbackErr error
|
|
wantErr bool
|
|
}{
|
|
{name: "success"},
|
|
{name: "unavailable", callbackErr: status.Error(codes.Unavailable, "finalize callback unavailable")},
|
|
{name: "error", callbackErr: callbackErr, wantErr: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
called := false
|
|
p := exporterprovider.New(nil, exporterprovider.WithFinalizeCallback(func(_ context.Context, exporterResponse map[string]string) error {
|
|
called = true
|
|
assert.NotEmpty(t, exporterResponse[exptypes.ExporterImageDescriptorKey])
|
|
return tc.callbackErr
|
|
}))
|
|
|
|
err := build(tc.name, p)
|
|
if tc.wantErr {
|
|
require.ErrorContains(t, err, tc.callbackErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
assert.True(t, called)
|
|
})
|
|
}
|
|
}
|
|
|
|
// moby/buildkit#1418
|
|
func testTarExporterSymlink(t *testing.T, sb integration.Sandbox) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
cmdStr := integration.UnixOrWindows(
|
|
`sh -c "echo -n first > foo;ln -s foo bar"`,
|
|
`cmd /C "echo first> foo && mklink bar foo"`,
|
|
)
|
|
run(cmdStr)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
var buf bytes.Buffer
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterTar,
|
|
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: &buf}),
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(buf.Bytes(), false)
|
|
require.NoError(t, err)
|
|
|
|
item, ok := m["foo"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
newLine := integration.UnixOrWindows("", " \r\n")
|
|
require.Equal(t, []byte("first"+newLine), item.Data)
|
|
|
|
item, ok = m["bar"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeSymlink, int32(item.Header.Typeflag))
|
|
require.Equal(t, "foo", item.Header.Linkname)
|
|
}
|
|
|
|
func testTarExporterWithSocket(t *testing.T, sb integration.Sandbox) {
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
alpine := llb.Image("docker.io/library/alpine:latest")
|
|
def, err := alpine.Run(llb.Args([]string{"sh", "-c", "nc -l -s local:/socket.sock & usleep 100000; kill %1"})).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterTar,
|
|
Attrs: map[string]string{},
|
|
Output: func(m map[string]string) (io.WriteCloser, error) {
|
|
return &iohelper.NopWriteCloser{Writer: io.Discard}, nil
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testTarExporterWithSocketCopy(t *testing.T, sb integration.Sandbox) {
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
alpine := llb.Image("docker.io/library/alpine:latest")
|
|
state := alpine.Run(llb.Args([]string{"sh", "-c", "nc -l -s local:/root/socket.sock & usleep 100000; kill %1"})).Root()
|
|
|
|
fa := llb.Copy(state, "/root", "/roo2", &llb.CopyInfo{})
|
|
|
|
scratchCopy := llb.Scratch().File(fa)
|
|
|
|
def, err := scratchCopy.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
|
require.NoError(t, err)
|
|
}
|