Files
buildkit/client/client_export_local_test.go
2026-07-27 18:41:37 +02:00

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)
}