mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
client: split client integration tests by area
Move client integration test declarations out of the large harness files into focused files for cache, exporters, sources, mounts, gateway behavior, and related client features. Keep registration lists grouped by source file, move non-gateway tests out of the gateway harness, and remove the now-empty build_test.go. This is intended as a mechanical split with no test logic changes. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
29 files changed
+18119
-17555
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,130 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func testCallInfo(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureInfo)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
_, err = c.Info(sb.Context())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testClientCustomGRPCOpts(t *testing.T, sb integration.Sandbox) {
|
||||
var interceptedMethods []string
|
||||
intercept := func(
|
||||
ctx context.Context,
|
||||
method string,
|
||||
req,
|
||||
reply any,
|
||||
cc *grpc.ClientConn,
|
||||
invoker grpc.UnaryInvoker,
|
||||
opts ...grpc.CallOption,
|
||||
) error {
|
||||
interceptedMethods = append(interceptedMethods, method)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
c, err := New(sb.Context(), sb.Address(), WithGRPCDialOption(grpc.WithChainUnaryInterceptor(intercept)))
|
||||
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)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, interceptedMethods, "/moby.buildkit.v1.Control/Solve")
|
||||
}
|
||||
|
||||
func testListenBuildHistoryExcludesSoftDeletedRecords(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// Create 3 completed builds so we have multiple history records.
|
||||
var buildRefs [3]string
|
||||
for i := range buildRefs {
|
||||
def, err := llb.Scratch().File(llb.Mkfile(fmt.Sprintf("file%d", i), 0o644, nil)).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
buildRefs[i] = identity.NewID()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{Ref: buildRefs[i]}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
refToDelete := buildRefs[1]
|
||||
|
||||
// Start a streaming listener on one specific ref. This increments the
|
||||
// internal reference count, which causes Delete to soft-delete instead
|
||||
// of removing the record from the database.
|
||||
listenerCtx, listenerCancel := context.WithCancelCause(sb.Context())
|
||||
defer listenerCancel(nil)
|
||||
|
||||
cl, err := c.ControlClient().ListenBuildHistory(listenerCtx, &controlapi.BuildHistoryRequest{
|
||||
Ref: refToDelete,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read the initial record so the listener is fully registered.
|
||||
_, err = cl.Recv()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Soft-delete: the record is marked deleted but stays in the DB because
|
||||
// the listener above still holds a reference.
|
||||
_, err = c.ControlClient().UpdateBuildHistory(sb.Context(), &controlapi.UpdateBuildHistoryRequest{
|
||||
Ref: refToDelete,
|
||||
Delete: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// List all remaining history records with a limit to trigger sorting.
|
||||
// Before the fix this panicked with a nil pointer dereference because
|
||||
// the soft-deleted record left a nil entry in the event slice.
|
||||
cl2, err := c.ControlClient().ListenBuildHistory(sb.Context(), &controlapi.BuildHistoryRequest{
|
||||
EarlyExit: true,
|
||||
Limit: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
gotRefs := map[string]bool{}
|
||||
for {
|
||||
resp, err := cl2.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
gotRefs[resp.Record.Ref] = true
|
||||
}
|
||||
|
||||
// The soft-deleted record must not appear in the results.
|
||||
assert.False(t, gotRefs[refToDelete], "soft-deleted ref %s should not appear in history list", refToDelete)
|
||||
assert.True(t, gotRefs[buildRefs[0]], "ref %s should appear in history list", buildRefs[0])
|
||||
assert.True(t, gotRefs[buildRefs[2]], "ref %s should appear in history list", buildRefs[2])
|
||||
|
||||
// Clean up the streaming listener.
|
||||
listenerCancel(nil)
|
||||
// Drain the stream so gRPC can clean up.
|
||||
for {
|
||||
if _, err := cl.Recv(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testCallDiskUsage(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
_, err = c.DiskUsage(sb.Context())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testCgroupParent(t *testing.T, sb integration.Sandbox) {
|
||||
if sb.Rootless() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
if _, err := os.Lstat("/sys/fs/cgroup/cgroup.subtree_control"); os.IsNotExist(err) {
|
||||
t.Skipf("test requires cgroup v2")
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
img := llb.Image("alpine:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string, ro ...llb.RunOption) {
|
||||
st = img.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
cgroupName := "test." + identity.NewID()
|
||||
|
||||
err = os.MkdirAll(filepath.Join("/sys/fs/cgroup", cgroupName), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(filepath.Join("/sys/fs/cgroup", cgroupName))
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
err = os.WriteFile(filepath.Join("/sys/fs/cgroup", cgroupName, "pids.max"), []byte("10"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
run(`sh -c "(for i in $(seq 1 10); do sleep 1 & done 2>first.error); cat /proc/self/cgroup >> first"`, llb.WithCgroupParent(cgroupName))
|
||||
run(`sh -c "(for i in $(seq 1 10); do sleep 1 & done 2>second.error); cat /proc/self/cgroup >> second"`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// neither process leaks parent cgroup name inside container
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "first"))
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, strings.TrimSpace(string(dt)), cgroupName)
|
||||
|
||||
dt2, err := os.ReadFile(filepath.Join(destDir, "second"))
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, strings.TrimSpace(string(dt2)), cgroupName)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "first.error"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, strings.TrimSpace(string(dt)), "Resource temporarily unavailable")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "second.error"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", strings.TrimSpace(string(dt)))
|
||||
}
|
||||
|
||||
func testLinuxResources(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
if sb.Rootless() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
if _, err := os.Lstat("/sys/fs/cgroup/cgroup.subtree_control"); os.IsNotExist(err) {
|
||||
t.Skipf("test requires cgroup v2")
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
img := llb.Image("alpine:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string, ro ...llb.RunOption) {
|
||||
st = img.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
// Test memory limit: set 64MiB and verify via cgroup
|
||||
run(`sh -c "cat /sys/fs/cgroup/memory.max > mem_limited"`, llb.MemoryLimit(64*1024*1024))
|
||||
run(`sh -c "cat /sys/fs/cgroup/memory.max > mem_default"`)
|
||||
|
||||
// Test CPU quota: set quota=50000 period=100000 (50% CPU) and verify
|
||||
run(`sh -c "cat /sys/fs/cgroup/cpu.max > cpu_limited"`, llb.CPUQuota(50000), llb.CPUPeriod(100000))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "mem_limited"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "67108864", strings.TrimSpace(string(dt)))
|
||||
|
||||
dt2, err := os.ReadFile(filepath.Join(destDir, "mem_default"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "max", strings.TrimSpace(string(dt2)))
|
||||
|
||||
dt3, err := os.ReadFile(filepath.Join(destDir, "cpu_limited"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "50000 100000", strings.TrimSpace(string(dt3)))
|
||||
}
|
||||
|
||||
func testPassthroughOp(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
ctx := sb.Context()
|
||||
busybox := llb.Image("busybox:latest")
|
||||
base := llb.Scratch().File(llb.Mkfile("/base", 0644, []byte("base")))
|
||||
|
||||
t.Run("requires returns receiver and builds non-output dependency", func(t *testing.T) {
|
||||
cacheID := "passthrough-requires-" + identity.NewID()
|
||||
nonOutputDep := busybox.Run(llb.Shlex(`sh -e -c "echo requires-dep-built > /cache/marker; echo should-not-export > /not-output"`))
|
||||
nonOutputDep.AddMount("/cache", llb.Scratch(), llb.AsPersistentCacheDir(cacheID, llb.CacheMountShared))
|
||||
|
||||
outDir := solveStateToLocalDir(ctx, t, c, base.Requires("test.passthrough.requires", nonOutputDep.Root()))
|
||||
requireLocalFile(t, outDir, "base", "base")
|
||||
require.NoFileExists(t, filepath.Join(outDir, "not-output"))
|
||||
|
||||
markerDir := solveStateToLocalDir(ctx, t, c, cacheMountFile(busybox, cacheID, "marker"))
|
||||
requireLocalFile(t, markerDir, "marker", "requires-dep-built\n")
|
||||
})
|
||||
|
||||
t.Run("direct passthrough returns selected outputs and builds non-output input", func(t *testing.T) {
|
||||
cacheID := "passthrough-direct-" + identity.NewID()
|
||||
nonOutputInput := busybox.Run(llb.Shlex(`sh -e -c "echo direct-input-built > /cache/marker; echo should-not-export > /not-output"`))
|
||||
nonOutputInput.AddMount("/cache", llb.Scratch(), llb.AsPersistentCacheDir(cacheID, llb.CacheMountShared))
|
||||
|
||||
pass := llb.NewPassthroughOp("test.passthrough.direct", []llb.PassthroughInput{
|
||||
{State: llb.Scratch().File(llb.Mkfile("/a", 0644, []byte("a"))), Output: true},
|
||||
{State: nonOutputInput.Root()},
|
||||
{State: llb.Scratch().File(llb.Mkfile("/c", 0644, []byte("c"))), Output: true},
|
||||
})
|
||||
|
||||
outA := llb.NewState(pass.Output())
|
||||
outC := llb.NewState(pass.OutputAt(1))
|
||||
out := llb.Scratch().
|
||||
File(llb.Copy(outA, "/a", "/a")).
|
||||
File(llb.Copy(outC, "/c", "/c"))
|
||||
|
||||
outDir := solveStateToLocalDir(ctx, t, c, out)
|
||||
requireLocalFile(t, outDir, "a", "a")
|
||||
requireLocalFile(t, outDir, "c", "c")
|
||||
require.NoFileExists(t, filepath.Join(outDir, "not-output"))
|
||||
|
||||
markerDir := solveStateToLocalDir(ctx, t, c, cacheMountFile(busybox, cacheID, "marker"))
|
||||
requireLocalFile(t, markerDir, "marker", "direct-input-built\n")
|
||||
})
|
||||
|
||||
t.Run("failing non-output dependency fails solve", func(t *testing.T) {
|
||||
fail := busybox.Run(llb.Shlex(`sh -e -c "exit 42"`)).Root()
|
||||
def, err := base.Requires("test.passthrough.fail", fail).Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: t.TempDir(),
|
||||
}},
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// testRelativeMountpoint is a test that relative paths for mountpoints don't
|
||||
// fail when runc is upgraded to at least rc95, which introduces an error when
|
||||
// mountpoints are not absolute. Relative paths should be transformed to
|
||||
// absolute points based on the llb.State's current working directory.
|
||||
func testRelativeMountpoint(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
id := identity.NewID()
|
||||
|
||||
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
||||
scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
"sh -c 'echo -n %s > /root/relpath/data'",
|
||||
`cmd /C echo %s > \\root\\relpath\\data`,
|
||||
)
|
||||
st := llb.Image(imgName).Dir("/root").Run(
|
||||
llb.Shlexf(cmdStr, id),
|
||||
).AddMount("relpath", scratch)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "data"))
|
||||
require.NoError(t, err)
|
||||
newLine := integration.UnixOrWindows("", " \r\n")
|
||||
require.Equal(t, dt, []byte(id+newLine))
|
||||
}
|
||||
|
||||
func testRelativeWorkDir(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
imgName := integration.UnixOrWindows(
|
||||
"docker.io/library/busybox:latest",
|
||||
"mcr.microsoft.com/windows/nanoserver:ltsc2022",
|
||||
)
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
`sh -c "pwd > /out/pwd"`,
|
||||
`cmd /C "cd > /out/pwd"`,
|
||||
)
|
||||
pwd := llb.Image(imgName).
|
||||
Dir("test1").
|
||||
Dir("test2").
|
||||
Run(llb.Shlex(cmdStr)).
|
||||
AddMount("/out", llb.Scratch())
|
||||
|
||||
def, err := pwd.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "pwd"))
|
||||
require.NoError(t, err)
|
||||
pathStr := integration.UnixOrWindows(
|
||||
"/test1/test2\n",
|
||||
"C:\\test1\\test2\r\n",
|
||||
)
|
||||
require.Equal(t, []byte(pathStr), dt)
|
||||
}
|
||||
|
||||
func testRunValidExitCodes(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// no exit codes specified, equivalent to [0]
|
||||
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
||||
out := llb.Image(imgName)
|
||||
shellPrefix := integration.UnixOrWindows("sh -c", "cmd /C")
|
||||
|
||||
out = out.Run(llb.Shlexf(`%s "exit 0"`, shellPrefix)).Root()
|
||||
out = out.Run(llb.Shlexf(`%s "exit 1"`, shellPrefix)).Root()
|
||||
def, err := out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "exit code: 1")
|
||||
|
||||
// empty exit codes, equivalent to [0]
|
||||
out = llb.Image(imgName)
|
||||
out = out.Run(llb.Shlexf(`%s "exit 0"`, shellPrefix), llb.ValidExitCodes()).Root()
|
||||
def, err = out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// if we expect non-zero, those non-zero codes should succeed
|
||||
out = llb.Image(imgName)
|
||||
out = out.Run(llb.Shlexf(`%s "exit 1"`, shellPrefix), llb.ValidExitCodes(1)).Root()
|
||||
out = out.Run(llb.Shlexf(`%s "exit 2"`, shellPrefix), llb.ValidExitCodes(2, 3)).Root()
|
||||
out = out.Run(llb.Shlexf(`%s "exit 3"`, shellPrefix), llb.ValidExitCodes(2, 3)).Root()
|
||||
def, err = out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// if we expect non-zero, returning zero should fail
|
||||
out = llb.Image(imgName)
|
||||
out = out.Run(llb.Shlexf(`%s "exit 0"`, shellPrefix), llb.ValidExitCodes(1)).Root()
|
||||
def, err = out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "exit code: 0")
|
||||
}
|
||||
|
||||
func testSecurityMode(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureSecurityMode)
|
||||
command := `sh -c 'cat /proc/self/status | grep CapEff | cut -f 2 > /out'`
|
||||
mode := llb.SecurityModeSandbox
|
||||
var allowedEntitlements []string
|
||||
var assertCaps func(caps uint64)
|
||||
secMode := sb.Value("secmode")
|
||||
if secMode == securitySandbox {
|
||||
assertCaps = func(caps uint64) {
|
||||
/*
|
||||
$ capsh --decode=00000000a80425fb
|
||||
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
|
||||
cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
|
||||
*/
|
||||
require.Equal(t, uint64(0xa80425fb), caps)
|
||||
}
|
||||
allowedEntitlements = []string{}
|
||||
} else {
|
||||
assertCaps = func(caps uint64) {
|
||||
/*
|
||||
$ capsh --decode=0000003fffffffff
|
||||
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
|
||||
cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,
|
||||
cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,
|
||||
cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,
|
||||
cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
|
||||
*/
|
||||
|
||||
// require that _at least_ minimum capabilities are granted
|
||||
require.Equal(t, uint64(0x3fffffffff), caps&0x3fffffffff)
|
||||
}
|
||||
mode = llb.SecurityModeInsecure
|
||||
allowedEntitlements = []string{entitlements.EntitlementSecurityInsecure.String()}
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(command),
|
||||
llb.Security(mode))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
AllowedEntitlements: allowedEntitlements,
|
||||
}, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
contents, err := os.ReadFile(filepath.Join(destDir, "out"))
|
||||
require.NoError(t, err)
|
||||
|
||||
caps, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 16, 64)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Caps: %x", caps)
|
||||
|
||||
assertCaps(caps)
|
||||
}
|
||||
|
||||
func testSecurityModeErrors(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
secMode := sb.Value("secmode")
|
||||
if secMode == securitySandbox {
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh -c 'echo sandbox'`))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
AllowedEntitlements: []string{entitlements.EntitlementSecurityInsecure.String()},
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "security.insecure is not allowed")
|
||||
}
|
||||
if secMode == securityInsecure {
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh -c 'echo insecure'`), llb.Security(llb.SecurityModeInsecure))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "security.insecure is not allowed")
|
||||
}
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex("echo invalid"))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundExec bool
|
||||
digestMap := map[digest.Digest]digest.Digest{}
|
||||
for i, dt := range def.Def {
|
||||
oldDigest := digest.FromBytes(dt)
|
||||
var op pb.Op
|
||||
require.NoError(t, op.UnmarshalVT(dt))
|
||||
changed := false
|
||||
|
||||
for j, input := range op.Inputs {
|
||||
if newDigest, ok := digestMap[digest.Digest(input.Digest)]; ok {
|
||||
op.Inputs[j].Digest = string(newDigest)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if execOp := op.GetExec(); execOp != nil {
|
||||
execOp.Security = pb.SecurityMode(2)
|
||||
changed = true
|
||||
foundExec = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
newDt, err := op.MarshalVT()
|
||||
require.NoError(t, err)
|
||||
def.Def[i] = newDt
|
||||
|
||||
newDigest := digest.FromBytes(newDt)
|
||||
digestMap[oldDigest] = newDigest
|
||||
if meta, ok := def.Metadata[oldDigest]; ok {
|
||||
delete(def.Metadata, oldDigest)
|
||||
def.Metadata[newDigest] = meta
|
||||
}
|
||||
}
|
||||
}
|
||||
require.True(t, foundExec)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid security mode")
|
||||
}
|
||||
|
||||
func testSecurityModeSysfs(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureSecurityMode)
|
||||
if sb.Rootless() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
mode := llb.SecurityModeSandbox
|
||||
var allowedEntitlements []string
|
||||
secMode := sb.Value("secmode")
|
||||
if secMode == securitySandbox {
|
||||
allowedEntitlements = []string{}
|
||||
} else {
|
||||
mode = llb.SecurityModeInsecure
|
||||
allowedEntitlements = []string{entitlements.EntitlementSecurityInsecure.String()}
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
cg := "/sys/fs/cgroup/cpuset/securitytest" // cgroup v1
|
||||
if _, err := os.Stat("/sys/fs/cgroup/cpuset"); errors.Is(err, os.ErrNotExist) {
|
||||
cg = "/sys/fs/cgroup/securitytest" // cgroup v2
|
||||
}
|
||||
|
||||
command := "mkdir " + cg
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(command),
|
||||
llb.Security(mode))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
AllowedEntitlements: allowedEntitlements,
|
||||
}, nil)
|
||||
|
||||
if secMode == securitySandbox {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "did not complete successfully")
|
||||
require.Contains(t, err.Error(), "mkdir "+cg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testShmSize(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").Run(
|
||||
llb.AddMount("/dev/shm", llb.Scratch(), llb.Tmpfs(llb.TmpfsSize(128*1024*1024))),
|
||||
llb.Shlex(`sh -c 'mount | grep /dev/shm > /out/out'`),
|
||||
)
|
||||
|
||||
out := st.AddMount("/out", llb.Scratch())
|
||||
def, err := out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "out"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(dt), `size=131072k`)
|
||||
}
|
||||
|
||||
// moby/buildkit#614
|
||||
func testStdinClosed(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")
|
||||
cmdStr := integration.UnixOrWindows("cat", "cmd /C more")
|
||||
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testUlimit(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
busybox := llb.Image("busybox:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string, ro ...llb.RunOption) {
|
||||
st = busybox.Run(append(ro, llb.Shlex(cmd), llb.Dir("/wd"))...).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
run(`sh -c "ulimit -n > first"`, llb.AddUlimit(llb.UlimitNofile, 1062, 1062))
|
||||
run(`sh -c "ulimit -n > second"`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "first"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `1062`, strings.TrimSpace(string(dt)))
|
||||
|
||||
dt2, err := os.ReadFile(filepath.Join(destDir, "second"))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, `1062`, strings.TrimSpace(string(dt2)))
|
||||
}
|
||||
|
||||
func testUser(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").Run(llb.Shlex(`sh -c "mkdir -m 0777 /wd"`))
|
||||
|
||||
run := func(user, cmd string) {
|
||||
if user != "" {
|
||||
st = st.Run(llb.Shlex(cmd), llb.Dir("/wd"), llb.User(user))
|
||||
} else {
|
||||
st = st.Run(llb.Shlex(cmd), llb.Dir("/wd"))
|
||||
}
|
||||
}
|
||||
|
||||
run("daemon", `sh -c "id -nu > user"`)
|
||||
run("daemon:daemon", `sh -c "id -ng > group"`)
|
||||
run("daemon:nobody", `sh -c "id -ng > nobody"`)
|
||||
run("1:1", `sh -c "id -g > userone"`)
|
||||
run("root", `sh -c "id -Gn > root_supplementary"`)
|
||||
run("", `sh -c "id -Gn > default_supplementary"`)
|
||||
run("", `rm /etc/passwd /etc/group`) // test that default user still works
|
||||
run("", `sh -c "id -u > default_uid"`)
|
||||
|
||||
st = st.Run(llb.Shlex("cp -a /wd/. /out/"))
|
||||
out := st.AddMount("/out", llb.Scratch())
|
||||
|
||||
def, err := out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "user"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "daemon", strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "group"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "daemon", strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "nobody"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "nobody", strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "userone"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1", strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "root_supplementary"))
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(dt), "root "))
|
||||
require.Contains(t, string(dt), "wheel")
|
||||
|
||||
dt2, err := os.ReadFile(filepath.Join(destDir, "default_supplementary"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(dt), string(dt2))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "default_uid"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0", strings.TrimSpace(string(dt)))
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func requireLocalFile(t *testing.T, dir, name, expected string) {
|
||||
t.Helper()
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dir, name))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, string(dt))
|
||||
}
|
||||
|
||||
func solveStateToLocalDir(ctx context.Context, t *testing.T, c *Client, st llb.State) string {
|
||||
t.Helper()
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
outDir := t.TempDir()
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: outDir,
|
||||
}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
return outDir
|
||||
}
|
||||
|
||||
type secModeSandbox struct{}
|
||||
|
||||
func (*secModeSandbox) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
type secModeInsecure struct{}
|
||||
|
||||
func (*secModeInsecure) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + "\n\ninsecure-entitlements = [\"security.insecure\"]\n", nil
|
||||
}
|
||||
|
||||
var (
|
||||
securitySandbox integration.ConfigUpdater = &secModeSandbox{}
|
||||
securityInsecure integration.ConfigUpdater = &secModeInsecure{}
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,910 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"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/require"
|
||||
)
|
||||
|
||||
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 testExportLocalModeInvalid(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 = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
Attrs: map[string]string{
|
||||
"mode": "backup",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, `invalid local exporter mode "backup"`)
|
||||
}
|
||||
|
||||
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 testExportTarPlatformIDSanitized(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()
|
||||
|
||||
const platformID = `..\buildkit-outside`
|
||||
platform := platforms.DefaultSpec()
|
||||
|
||||
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
st := llb.Scratch().File(
|
||||
llb.Mkfile("payload.txt", 0600, []byte("payload")),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
res := gateway.NewResult()
|
||||
res.AddRef(platformID, ref)
|
||||
|
||||
dt, err := json.Marshal(&exptypes.Platforms{
|
||||
Platforms: []exptypes.Platform{{
|
||||
ID: platformID,
|
||||
Platform: platform,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
outW := bytes.NewBuffer(nil)
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterTar,
|
||||
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
|
||||
},
|
||||
},
|
||||
}, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
for name := range m {
|
||||
require.Falsef(t, strings.HasPrefix(name, "../") ||
|
||||
strings.HasPrefix(name, `..\`) ||
|
||||
strings.Contains(name, `/../`) ||
|
||||
strings.Contains(name, `\..\`) ||
|
||||
strings.Contains(name, `\`) ||
|
||||
strings.Contains(name, ":"),
|
||||
"tar exporter emitted unsafe platform path %q", name)
|
||||
}
|
||||
|
||||
tarPaths := make([]string, 0, len(m))
|
||||
for name := range m {
|
||||
tarPaths = append(tarPaths, name)
|
||||
}
|
||||
sort.Strings(tarPaths)
|
||||
|
||||
expectedPath := path.Join(".._buildkit-outside", integration.UnixOrWindows("payload.txt", "Files/payload.txt"))
|
||||
item := m[expectedPath]
|
||||
require.NotNilf(t, item, "expected sanitized tar path %q in %v", expectedPath, tarPaths)
|
||||
require.Equal(t, "payload", string(item.Data))
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,936 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"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 testCopyFromEmptyImage(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// On Windows, the error messages are different for the Scratch image
|
||||
// (which is coming from the OS). While the one for the no-layers image
|
||||
// is being returned by Buildkit (in unix style).
|
||||
winErrMsgs := []string{
|
||||
"foo: The system cannot find the file specified", // for llb.Scratch()
|
||||
"/foo: no such file or directory", // for tonistiigi/test:nolayers
|
||||
}
|
||||
|
||||
for i, image := range []llb.State{llb.Scratch(), llb.Image("tonistiigi/test:nolayers")} {
|
||||
st := llb.Scratch().File(llb.Copy(image, "/", "/"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
st = llb.Scratch().File(llb.Copy(image, "/foo", "/"))
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
errMsg := integration.UnixOrWindows(
|
||||
"/foo: no such file or directory",
|
||||
winErrMsgs[i],
|
||||
)
|
||||
require.Contains(t, err.Error(), errMsg)
|
||||
|
||||
imgName := integration.UnixOrWindows(
|
||||
"busybox:latest",
|
||||
"nanoserver:latest",
|
||||
)
|
||||
busybox := llb.Image(imgName)
|
||||
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
`sh -e -c '[ $(ls /scratch | wc -l) = '0' ]'`,
|
||||
`cmd /C dir \scratch`,
|
||||
)
|
||||
out := busybox.Run(llb.Shlex(cmdStr))
|
||||
out.AddMount("/scratch", image, llb.Readonly)
|
||||
|
||||
def, err = out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// containerd/containerd#2119
|
||||
func testDuplicateWhiteouts(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
busybox := llb.Image("busybox:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
run(`sh -e -c "mkdir -p d0 d1; echo -n first > d1/bar;"`)
|
||||
run(`sh -c "rm -rf d0 d1"`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Output: fixedWriteCloser(outW),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
||||
require.NoError(t, err)
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst)
|
||||
require.NoError(t, err)
|
||||
|
||||
lastLayer := mfst.Layers[len(mfst.Layers)-1]
|
||||
|
||||
layer, ok := m[ocispecs.ImageBlobsDir+"/sha256/"+lastLayer.Digest.Hex()]
|
||||
require.True(t, ok)
|
||||
|
||||
m, err = testutil.ReadTarToMap(layer.Data, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok = m[".wh.d0"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m[".wh.d1"]
|
||||
require.True(t, ok)
|
||||
|
||||
// check for a bug that added whiteout for subfile
|
||||
_, ok = m["d1/.wh.bar"]
|
||||
require.True(t, !ok)
|
||||
}
|
||||
|
||||
func testFileOpCopyAlwaysReplaceExistingDestPaths(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
destDirHostPath := integration.Tmpdir(t,
|
||||
fstest.CreateDir("root", 0755),
|
||||
fstest.CreateDir("root/overwritedir", 0755),
|
||||
fstest.CreateFile("root/overwritedir/subfile", nil, 0755),
|
||||
fstest.CreateFile("root/overwritefile", nil, 0755),
|
||||
fstest.Symlink("dir", "root/overwritesymlink"),
|
||||
fstest.CreateDir("root/dir", 0755),
|
||||
fstest.CreateFile("root/dir/dirfile1", nil, 0755),
|
||||
fstest.CreateDir("root/dir/overwritesubdir", 0755),
|
||||
fstest.CreateFile("root/dir/overwritesubfile", nil, 0755),
|
||||
fstest.Symlink("dirfile1", "root/dir/overwritesymlink"),
|
||||
)
|
||||
destDir := llb.Local("destDir")
|
||||
|
||||
srcDirHostPath := integration.Tmpdir(t,
|
||||
fstest.CreateDir("root", 0755),
|
||||
fstest.CreateFile("root/overwritedir", nil, 0755),
|
||||
fstest.CreateDir("root/overwritefile", 0755),
|
||||
fstest.CreateFile("root/overwritefile/foo", nil, 0755),
|
||||
fstest.CreateDir("root/overwritesymlink", 0755),
|
||||
fstest.CreateDir("root/dir", 0755),
|
||||
fstest.CreateFile("root/dir/dirfile2", nil, 0755),
|
||||
fstest.CreateFile("root/dir/overwritesubdir", nil, 0755),
|
||||
fstest.CreateDir("root/dir/overwritesubfile", 0755),
|
||||
fstest.CreateDir("root/dir/overwritesymlink", 0755),
|
||||
)
|
||||
srcDir := llb.Local("srcDir")
|
||||
|
||||
resultDir := destDir.File(llb.Copy(srcDir, "/", "/", &llb.CopyInfo{
|
||||
CopyDirContentsOnly: true,
|
||||
AlwaysReplaceExistingDestPaths: true,
|
||||
}))
|
||||
|
||||
def, err := resultDir.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
resultDirHostPath := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: resultDirHostPath,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"destDir": destDirHostPath,
|
||||
"srcDir": srcDirHostPath,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fstest.CheckDirectoryEqualWithApplier(resultDirHostPath, fstest.Apply(
|
||||
fstest.CreateDir("root", 0755),
|
||||
fstest.CreateFile("root/overwritedir", nil, 0755),
|
||||
fstest.CreateDir("root/overwritefile", 0755),
|
||||
fstest.CreateFile("root/overwritefile/foo", nil, 0755),
|
||||
fstest.CreateDir("root/overwritesymlink", 0755),
|
||||
fstest.CreateDir("root/dir", 0755),
|
||||
fstest.CreateFile("root/dir/dirfile1", nil, 0755),
|
||||
fstest.CreateFile("root/dir/dirfile2", nil, 0755),
|
||||
fstest.CreateFile("root/dir/overwritesubdir", nil, 0755),
|
||||
fstest.CreateDir("root/dir/overwritesubfile", 0755),
|
||||
fstest.CreateDir("root/dir/overwritesymlink", 0755),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testFileOpCopyChmodText(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
tcases := []struct {
|
||||
src string
|
||||
dest string
|
||||
mode string
|
||||
}{
|
||||
{"file", "f1", "go-w"},
|
||||
{"file", "f2", "o-rwx,g+x"},
|
||||
{"file", "f3", "u=rwx,g=,o="},
|
||||
{"file", "f4", "u+rw,g+r,o-x,o+w"},
|
||||
{"dir", "d1", "a+X"},
|
||||
{"dir", "d2", "g+rw,o+rw"},
|
||||
{"dir", "d3", "u=rwX,go=rX"},
|
||||
}
|
||||
|
||||
st := llb.Image("alpine").
|
||||
Run(llb.Shlex(`sh -c "mkdir /input && touch /input/file && mkdir /input/dir && chmod 0500 /input/dir && mkdir /expected"`))
|
||||
|
||||
for _, tc := range tcases {
|
||||
st = st.Run(llb.Shlex(`sh -c "cp -a /input/` + tc.src + ` /expected/` + tc.dest + ` && chmod ` + tc.mode + ` /expected/` + tc.dest + `"`))
|
||||
}
|
||||
cp := llb.Scratch()
|
||||
|
||||
for _, tc := range tcases {
|
||||
cp = cp.File(llb.Copy(st.Root(), "/input/"+tc.src, "/"+tc.dest, llb.ChmodOpt{ModeStr: tc.mode}))
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
st = st.Run(llb.Shlex(`sh -c '[ "$(stat -c '%A' /expected/`+tc.dest+`)" == "$(stat -c '%A' /actual/`+tc.dest+`)" ]'`), llb.AddMount("/actual", cp, llb.Readonly))
|
||||
}
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testFileOpCopyIncludeExclude(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("myfile", []byte("data0"), 0600),
|
||||
fstest.CreateDir("sub", 0700),
|
||||
fstest.CreateFile("sub/foo", []byte("foo0"), 0600),
|
||||
fstest.CreateFile("sub/bar", []byte("bar0"), 0600),
|
||||
)
|
||||
|
||||
st := llb.Scratch().File(
|
||||
llb.Copy(
|
||||
llb.Local("mylocal"), "/", "/", &llb.CopyInfo{
|
||||
IncludePatterns: []string{"sub/*"},
|
||||
ExcludePatterns: []string{"sub/bar"},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
busybox := llb.Image("busybox:latest")
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
||||
}
|
||||
run(`sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "sub", "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("foo0"), dt)
|
||||
|
||||
for _, name := range []string{"myfile", "sub/bar"} {
|
||||
_, err = os.Stat(filepath.Join(destDir, name))
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
randBytes, err := os.ReadFile(filepath.Join(destDir, "unique"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create additional file which doesn't match the include pattern, and make
|
||||
// sure this doesn't invalidate the cache.
|
||||
|
||||
err = fstest.Apply(fstest.CreateFile("unmatchedfile", []byte("data1"), 0600)).Apply(dir.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
st = llb.Scratch().File(
|
||||
llb.Copy(
|
||||
llb.Local("mylocal"), "/", "/", &llb.CopyInfo{
|
||||
IncludePatterns: []string{"sub/*"},
|
||||
ExcludePatterns: []string{"sub/bar"},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
run(`sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
randBytes2, err := os.ReadFile(filepath.Join(destDir, "unique"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, randBytes, randBytes2)
|
||||
}
|
||||
|
||||
func testFileOpCopyRm(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("myfile", []byte("data0"), 0600),
|
||||
fstest.CreateDir("sub", 0700),
|
||||
fstest.CreateFile("sub/foo", []byte("foo0"), 0600),
|
||||
fstest.CreateFile("sub/bar", []byte("bar0"), 0600),
|
||||
)
|
||||
dir2 := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("file2", []byte("file2"), 0600),
|
||||
)
|
||||
|
||||
st := llb.Scratch().
|
||||
File(
|
||||
llb.Copy(llb.Local("mylocal"), "myfile", "myfile2").
|
||||
Copy(llb.Local("mylocal"), "sub", "out").
|
||||
Rm("out/foo").
|
||||
Copy(llb.Local("mylocal2"), "file2", "/"))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
"mylocal2": dir2,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "myfile2"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("data0"), dt)
|
||||
|
||||
fi, err := os.Stat(filepath.Join(destDir, "out"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, fi.IsDir())
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "out/bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("bar0"), dt)
|
||||
|
||||
_, err = os.Stat(filepath.Join(destDir, "out/foo"))
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "file2"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("file2"), dt)
|
||||
}
|
||||
|
||||
// moby/buildkit#3291
|
||||
func testFileOpCopyUIDCache(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Scratch().File(
|
||||
llb.Copy(llb.Image("alpine").Run(llb.Shlex(`sh -c 'echo 123 > /foo && chown 1000:1000 /foo'`)).Root(), "foo", "foo"))
|
||||
|
||||
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)
|
||||
|
||||
fi, ok := m["foo"]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1000, fi.Header.Uid)
|
||||
require.Equal(t, 1000, fi.Header.Gid)
|
||||
|
||||
// repeat to check cache does not apply for different uid
|
||||
st = llb.Scratch().File(
|
||||
llb.Copy(llb.Image("alpine").Run(llb.Shlex(`sh -c 'echo 123 > /foo'`)).Root(), "foo", "foo"))
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
fi, ok = m["foo"]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, fi.Header.Uid)
|
||||
require.Equal(t, 0, fi.Header.Gid)
|
||||
}
|
||||
|
||||
// testFileOpInputSwap is a regression test that cache is invalidated when subset of fileop is built
|
||||
func testFileOpInputSwap(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
base := llb.Scratch().File(llb.Mkfile("/foo", 0600, []byte("foo")))
|
||||
|
||||
src := llb.Scratch().File(llb.Mkfile("/bar", 0600, []byte("bar")))
|
||||
|
||||
st := base.File(llb.Copy(src, "/bar", "/baz"))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// bar does not exist in base but index of all inputs remains the same
|
||||
st = base.File(llb.Copy(base, "/bar", "/baz"))
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
errStr := integration.UnixOrWindows(
|
||||
"bar: no such file",
|
||||
"bar: The system cannot find the file specified",
|
||||
)
|
||||
require.Contains(t, err.Error(), errStr)
|
||||
}
|
||||
|
||||
func testFileOpMkdirMkfile(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Scratch().
|
||||
File(llb.Mkdir("/foo", 0700).Mkfile("bar", 0600, []byte("contents")))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
fi, err := os.Stat(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, fi.IsDir())
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("contents"), dt)
|
||||
}
|
||||
|
||||
func testFileOpRmWildcard(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateDir("foo", 0700),
|
||||
fstest.CreateDir("bar", 0700),
|
||||
fstest.CreateFile("foo/target", []byte("foo0"), 0600),
|
||||
fstest.CreateFile("bar/target", []byte("bar0"), 0600),
|
||||
fstest.CreateFile("bar/remaining", []byte("bar1"), 0600),
|
||||
)
|
||||
|
||||
st := llb.Scratch().File(
|
||||
llb.Copy(llb.Local("mylocal"), "foo", "foo").
|
||||
Copy(llb.Local("mylocal"), "bar", "bar"),
|
||||
).File(
|
||||
llb.Rm("*/target", llb.WithAllowWildcard(true)),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "bar/remaining"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("bar1"), dt)
|
||||
|
||||
fi, err := os.Stat(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, fi.IsDir())
|
||||
|
||||
_, err = os.Stat(filepath.Join(destDir, "foo/target"))
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
_, err = os.Stat(filepath.Join(destDir, "bar/target"))
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func testFileOpSymlink(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
const (
|
||||
fileOwner = 7777
|
||||
fileGroup = 8888
|
||||
linkOwner = 1111
|
||||
linkGroup = 2222
|
||||
|
||||
dummyTimestamp = 42
|
||||
)
|
||||
|
||||
dummyTime := time.Unix(dummyTimestamp, 0)
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Scratch().
|
||||
File(llb.Mkdir("/foo", 0700).Mkfile("bar", 0600, []byte("contents"), llb.ChownOpt{
|
||||
User: &llb.UserOpt{
|
||||
UID: fileOwner,
|
||||
},
|
||||
Group: &llb.UserOpt{
|
||||
UID: fileGroup,
|
||||
},
|
||||
})).
|
||||
File(llb.Symlink("bar", "/baz", llb.WithCreatedTime(dummyTime), llb.ChownOpt{
|
||||
User: &llb.UserOpt{
|
||||
UID: linkOwner,
|
||||
},
|
||||
Group: &llb.UserOpt{
|
||||
UID: linkGroup,
|
||||
},
|
||||
}))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
defer outW.Close()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterTar,
|
||||
Output: fixedWriteCloser(outW),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
entry, ok := m["bar"]
|
||||
require.True(t, ok)
|
||||
|
||||
dt = entry.Data
|
||||
header := entry.Header
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []byte("contents"), dt)
|
||||
require.Equal(t, fileOwner, header.Uid)
|
||||
require.Equal(t, fileGroup, header.Gid)
|
||||
|
||||
entry, ok = m["baz"]
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
header = entry.Header
|
||||
// ensure it is a symlink
|
||||
require.Equal(t, tar.TypeSymlink, rune(header.Typeflag))
|
||||
// ensure it is a symlink to the proper location
|
||||
require.Equal(t, "bar", header.Linkname)
|
||||
|
||||
// make sure it was chowned properly
|
||||
require.Equal(t, linkOwner, header.Uid)
|
||||
require.Equal(t, linkGroup, header.Gid)
|
||||
|
||||
// ensure it was timestamped properly
|
||||
require.Equal(t, dummyTime, header.ModTime)
|
||||
}
|
||||
|
||||
// #2490
|
||||
func testMoveParentDir(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")
|
||||
busybox := llb.Image(imgName)
|
||||
st := llb.Scratch()
|
||||
|
||||
switch imgName {
|
||||
case "nanoserver:latest":
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).Root()
|
||||
}
|
||||
|
||||
run(`cmd /c "mkdir foo && echo first > foo/bar && move foo foo2"`)
|
||||
case "busybox:latest":
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
run(`sh -c "mkdir -p foo; echo -n first > foo/bar;"`)
|
||||
run(`mv foo foo2`)
|
||||
}
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Output: fixedWriteCloser(outW),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
||||
require.NoError(t, err)
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst)
|
||||
require.NoError(t, err)
|
||||
|
||||
lastLayer := mfst.Layers[len(mfst.Layers)-1]
|
||||
|
||||
layer, ok := m[ocispecs.ImageBlobsDir+"/sha256/"+lastLayer.Digest.Hex()]
|
||||
require.True(t, ok)
|
||||
|
||||
m, err = testutil.ReadTarToMap(layer.Data, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
switch imgName {
|
||||
case "nanoserver:latest":
|
||||
_, ok = m["/wd/foo/bar"] // Should be false, else move didn't happen
|
||||
require.False(t, ok)
|
||||
|
||||
_, ok = m["Files/wd/foo/bar"] // Should be false, else move didn't happen
|
||||
require.False(t, ok)
|
||||
|
||||
_, ok = m["Files/wd/foo2/bar"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m["Files/wd/foo2"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m["Files/wd"]
|
||||
require.True(t, ok)
|
||||
|
||||
for key := range m {
|
||||
if err == nil && strings.Contains(key, "/wd") {
|
||||
ok = true
|
||||
break
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
require.True(t, ok)
|
||||
|
||||
for key := range m {
|
||||
if err == nil && strings.Contains(key, "/foo2/bar") {
|
||||
ok = true
|
||||
break
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
require.True(t, ok)
|
||||
|
||||
for key := range m {
|
||||
if err == nil && strings.Contains(key, "/foo2") {
|
||||
ok = true
|
||||
break
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
require.True(t, ok)
|
||||
case "busybox:latest":
|
||||
_, ok = m[".wh.foo"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m["foo2/"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m["foo2/bar"]
|
||||
require.True(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func testRmSymlink(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// Test that if FileOp.Rm is called on a symlink, then
|
||||
// the symlink is removed rather than the target
|
||||
type testVars struct {
|
||||
imgName string
|
||||
touchCmd string
|
||||
symlinkCmd string
|
||||
}
|
||||
|
||||
v := integration.UnixOrWindows(
|
||||
testVars{
|
||||
imgName: "alpine",
|
||||
touchCmd: "touch /mnt/target",
|
||||
symlinkCmd: "ln -s target /mnt/link",
|
||||
},
|
||||
testVars{
|
||||
imgName: "nanoserver:latest",
|
||||
touchCmd: "cmd /C type nul > /mnt/target",
|
||||
symlinkCmd: "cmd /c cd /d c:/mnt && mklink link target",
|
||||
},
|
||||
)
|
||||
|
||||
mnt := llb.Image(v.imgName).
|
||||
Run(llb.Shlex(v.touchCmd)).
|
||||
AddMount("/mnt", llb.Scratch())
|
||||
|
||||
mnt = llb.Image(v.imgName).
|
||||
Run(llb.Shlex(v.symlinkCmd)).
|
||||
AddMount("/mnt", mnt)
|
||||
|
||||
def, err := mnt.File(llb.Rm("link")).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, fstest.CheckDirectoryEqualWithApplier(destDir, fstest.CreateFile("target", nil, 0644)))
|
||||
}
|
||||
|
||||
// #276
|
||||
func testWhiteoutParentDir(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
busybox := llb.Image("busybox:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
run(`sh -c "mkdir -p foo; echo -n first > foo/bar;"`)
|
||||
run(`rm foo/bar`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Output: fixedWriteCloser(outW),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
||||
require.NoError(t, err)
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst)
|
||||
require.NoError(t, err)
|
||||
|
||||
lastLayer := mfst.Layers[len(mfst.Layers)-1]
|
||||
|
||||
layer, ok := m[ocispecs.ImageBlobsDir+"/sha256/"+lastLayer.Digest.Hex()]
|
||||
require.True(t, ok)
|
||||
|
||||
m, err = testutil.ReadTarToMap(layer.Data, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok = m["foo/.wh.bar"]
|
||||
require.True(t, ok)
|
||||
|
||||
_, ok = m["foo/"]
|
||||
require.True(t, ok)
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
ctd "github.com/containerd/containerd/v2/client"
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"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/require"
|
||||
)
|
||||
|
||||
func testFrontendImageNaming(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
checkImageName := map[string]func(out, imageName string, exporterResponse map[string]string){
|
||||
ExporterOCI: func(out, imageName string, exporterResponse map[string]string) {
|
||||
// Nothing to check
|
||||
},
|
||||
ExporterDocker: func(out, imageName string, exporterResponse map[string]string) {
|
||||
require.Contains(t, exporterResponse, exptypes.ExporterImageNameKey)
|
||||
require.Equal(t, exporterResponse[exptypes.ExporterImageNameKey], "docker.io/library/"+imageName)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := m["oci-layout"]
|
||||
require.True(t, ok)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, index.SchemaVersion)
|
||||
require.Equal(t, 1, len(index.Manifests))
|
||||
|
||||
var dockerMfst []struct {
|
||||
RepoTags []string
|
||||
}
|
||||
err = json.Unmarshal(m["manifest.json"].Data, &dockerMfst)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(dockerMfst))
|
||||
require.Equal(t, 1, len(dockerMfst[0].RepoTags))
|
||||
require.Equal(t, imageName, dockerMfst[0].RepoTags[0])
|
||||
},
|
||||
ExporterImage: func(_, imageName string, exporterResponse map[string]string) {
|
||||
require.Contains(t, exporterResponse, exptypes.ExporterImageNameKey)
|
||||
require.Equal(t, exporterResponse[exptypes.ExporterImageNameKey], imageName)
|
||||
|
||||
// check if we can pull (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 := ctd.New(cdAddress)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
||||
|
||||
// check image in containerd
|
||||
_, err = client.ImageService().Get(ctx, imageName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// deleting image should release all content
|
||||
err = client.ImageService().Delete(ctx, imageName, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
_, err = client.Pull(ctx, imageName)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.ImageService().Delete(ctx, imageName, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
},
|
||||
}
|
||||
|
||||
// A caller provided name takes precedence over one returned by the frontend. Iterate over both options.
|
||||
for _, winner := range []string{"frontend", "caller"} {
|
||||
// The double layer of `t.Run` here is required so
|
||||
// that the inner-most tests (with the actual
|
||||
// functionality) have definitely completed before the
|
||||
// sandbox and registry cleanups (defered above) are run.
|
||||
t.Run(winner, func(t *testing.T) {
|
||||
for _, exp := range []string{ExporterOCI, ExporterDocker, ExporterImage} {
|
||||
t.Run(exp, func(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
|
||||
so := SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: exp,
|
||||
Attrs: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
|
||||
imageName := "image-" + exp + "-fe:latest"
|
||||
|
||||
switch exp {
|
||||
case ExporterOCI:
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
||||
t.Skip("oci exporter does not support named images")
|
||||
case ExporterDocker:
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
so.Exports[0].Output = fixedWriteCloser(outW)
|
||||
case ExporterImage:
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
imageName = registry + "/" + imageName
|
||||
so.Exports[0].Attrs["push"] = "true"
|
||||
}
|
||||
|
||||
feName := imageName
|
||||
switch winner {
|
||||
case "caller":
|
||||
feName = "loser:latest"
|
||||
so.Exports[0].Attrs["name"] = imageName
|
||||
case "frontend":
|
||||
so.Exports[0].Attrs["name"] = "*"
|
||||
}
|
||||
|
||||
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
res := gateway.NewResult()
|
||||
res.AddMeta("image.name", []byte(feName))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
resp, err := c.Build(sb.Context(), so, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkImageName[exp](out, imageName, resp.ExporterResponse)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testFrontendLintSkipVerifyPlatforms(t *testing.T, sb integration.Sandbox) {
|
||||
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) {
|
||||
dockerfile := `
|
||||
FROM scratch
|
||||
COPY foo /foo
|
||||
`
|
||||
st := llb.Scratch().File(
|
||||
llb.Mkfile("Dockerfile", 0600, []byte(dockerfile)).
|
||||
Mkfile("foo", 0600, []byte("data")))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
FrontendInputs: map[string]*pb.Definition{
|
||||
"context": def.ToPB(),
|
||||
"dockerfile": def.ToPB(),
|
||||
},
|
||||
FrontendOpt: map[string]string{
|
||||
"requestid": "frontend.lint",
|
||||
"frontend.caps": "moby.buildkit.frontend.subrequests",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
wc := newWarningsCapture()
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"platform": "linux/amd64,linux/arm64",
|
||||
},
|
||||
}, "", frontend, wc.status)
|
||||
require.NoError(t, err)
|
||||
warnings := wc.wait()
|
||||
|
||||
for _, w := range warnings {
|
||||
t.Logf("warning: %s", string(w.Short))
|
||||
}
|
||||
require.Len(t, warnings, 0)
|
||||
}
|
||||
|
||||
func testFrontendMetadataReturn(t *testing.T, sb integration.Sandbox) {
|
||||
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) {
|
||||
res := gateway.NewResult()
|
||||
res.AddMeta("frontend.returned", []byte("true"))
|
||||
res.AddMeta("not-frontend.not-returned", []byte("false"))
|
||||
res.AddMeta("frontendnot.returned.either", []byte("false"))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var exports []ExportEntry
|
||||
if workers.IsTestDockerdMoby(sb) {
|
||||
exports = []ExportEntry{{
|
||||
Type: "moby",
|
||||
Attrs: map[string]string{
|
||||
"name": "reg.dummy:5000/buildkit/test:latest",
|
||||
},
|
||||
}}
|
||||
} else {
|
||||
exports = []ExportEntry{{
|
||||
Type: ExporterOCI,
|
||||
Attrs: map[string]string{},
|
||||
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: io.Discard}),
|
||||
}}
|
||||
}
|
||||
|
||||
res, err := c.Build(sb.Context(), SolveOpt{
|
||||
Exports: exports,
|
||||
}, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, res.ExporterResponse, "frontend.returned")
|
||||
require.Equal(t, "true", res.ExporterResponse["frontend.returned"])
|
||||
require.NotContains(t, res.ExporterResponse, "not-frontend.not-returned")
|
||||
require.NotContains(t, res.ExporterResponse, "frontendnot.returned.either")
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testFrontendUseSolveResults(t *testing.T, sb integration.Sandbox) {
|
||||
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("data")),
|
||||
)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref, err := res.SingleRef()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st2, err := ref.ToState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st = llb.Scratch().File(
|
||||
llb.Copy(st2, "foo", "foo2"),
|
||||
)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
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,
|
||||
},
|
||||
},
|
||||
}, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "foo2"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dt, []byte("data"))
|
||||
}
|
||||
|
||||
func testFrontendVerifyPlatforms(t *testing.T, sb integration.Sandbox) {
|
||||
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("data")),
|
||||
)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}
|
||||
|
||||
wc := newWarningsCapture()
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"platform": "linux/amd64,linux/arm64",
|
||||
},
|
||||
}, "", frontend, wc.status)
|
||||
require.NoError(t, err)
|
||||
warnings := wc.wait()
|
||||
|
||||
require.Len(t, warnings, 1)
|
||||
require.Contains(t, string(warnings[0].Short), "Multiple platforms requested but result is not multi-platform")
|
||||
|
||||
wc = newWarningsCapture()
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
FrontendAttrs: map[string]string{},
|
||||
}, "", frontend, wc.status)
|
||||
require.NoError(t, err)
|
||||
|
||||
warnings = wc.wait()
|
||||
require.Len(t, warnings, 0, warningsListOutput(warnings))
|
||||
|
||||
frontend = func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
res := gateway.NewResult()
|
||||
platformsToTest := []string{"linux/amd64", "linux/arm64"}
|
||||
expPlatforms := &exptypes.Platforms{
|
||||
Platforms: make([]exptypes.Platform, len(platformsToTest)),
|
||||
}
|
||||
for i, platform := range platformsToTest {
|
||||
st := llb.Scratch().File(
|
||||
llb.Mkfile("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
|
||||
}
|
||||
|
||||
wc = newWarningsCapture()
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"platform": "linux/amd64,linux/arm64",
|
||||
},
|
||||
}, "", frontend, wc.status)
|
||||
require.NoError(t, err)
|
||||
warnings = wc.wait()
|
||||
|
||||
require.Len(t, warnings, 0)
|
||||
|
||||
wc = newWarningsCapture()
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
FrontendAttrs: map[string]string{},
|
||||
}, "", frontend, wc.status)
|
||||
require.NoError(t, err)
|
||||
|
||||
warnings = wc.wait()
|
||||
require.Len(t, warnings, 1)
|
||||
require.Contains(t, string(warnings[0].Short), "do not match result platforms linux/amd64,linux/arm64")
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/plugins/content/local"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/moby/buildkit/util/gitutil/gitobject"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testGitBundleRoundTrip exercises the bundle round-trip end to end:
|
||||
//
|
||||
// 1. Seed a git repo served over HTTP.
|
||||
// 2. Build 1 uses GitCheckoutBundle() to export a single-file git bundle.
|
||||
// 3. Build 2 imports that bundle via GitBundleURL("oci-layout+blob://...")
|
||||
// against a client-side OCI layout store, once with the sha pinned as ref
|
||||
// and once with a symbolic ref ("master"). Both checkouts must succeed
|
||||
// and produce a worktree matching Build 0 (plain git clone).
|
||||
// 4. A KeepGitDir subtest exercises llb.KeepGitDir() + bundle and asserts
|
||||
// no fake refs/buildkit/* namespace leaks into the checkout's .git dir.
|
||||
// 5. A provenance subtest enables attest:provenance and asserts both the
|
||||
// git-commit material and the bundle-blob material are recorded.
|
||||
func testGitBundleRoundTrip(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCILayout)
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
gitDir := t.TempDir()
|
||||
err = runInDir(gitDir,
|
||||
"git init -b master",
|
||||
"git config --local user.email test@example.com",
|
||||
"git config --local user.name test",
|
||||
"echo hello > file.txt",
|
||||
"git add file.txt",
|
||||
"git commit -m initial",
|
||||
"git update-server-info",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD")
|
||||
cmd.Dir = gitDir
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err)
|
||||
headSha := strings.TrimSpace(string(out))
|
||||
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
|
||||
defer server.Close()
|
||||
repoURL := server.URL + "/.git"
|
||||
|
||||
// Build 1: export a bundle via GitCheckoutBundle.
|
||||
bundleDir := t.TempDir()
|
||||
st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
|
||||
def, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, bundleBytes)
|
||||
bundleDgst := digest.FromBytes(bundleBytes)
|
||||
|
||||
// Sanity-check the bundle locally: it must contain the commit we pinned,
|
||||
// landing in the natural ref namespace (refs/heads/master).
|
||||
localBare := t.TempDir()
|
||||
err = runInDir(localBare, "git init --bare")
|
||||
require.NoError(t, err)
|
||||
bundleRoot := t.TempDir()
|
||||
root, err := os.OpenRoot(bundleRoot)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, root.WriteFile("bundle.pack", bundleBytes, 0644))
|
||||
require.NoError(t, root.Close())
|
||||
bundlePath := filepath.Join(bundleRoot, "bundle.pack")
|
||||
err = runInDir(localBare, fmt.Sprintf("git fetch %s +refs/*:refs/*", bundlePath))
|
||||
require.NoError(t, err)
|
||||
//nolint:gosec // Test-controlled temp dir and commit SHA are not attacker input.
|
||||
cmd = exec.CommandContext(context.TODO(), "git", "--git-dir="+localBare, "cat-file", "-e", headSha+"^{commit}")
|
||||
require.NoError(t, cmd.Run(), "bundle does not contain expected commit")
|
||||
//nolint:gosec // Test-controlled temp dir is not attacker input.
|
||||
cmd = exec.CommandContext(context.TODO(), "git", "--git-dir="+localBare, "rev-parse", "--verify", "refs/heads/master")
|
||||
masterOut, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "bundle should carry user's ref refs/heads/master; output=%q", string(masterOut))
|
||||
require.Equal(t, headSha, strings.TrimSpace(string(masterOut)))
|
||||
|
||||
// Stage the bundle in a client-side OCI layout store so Build 2 can
|
||||
// resolve it through an oci-layout+blob:// locator.
|
||||
storeDir := t.TempDir()
|
||||
store, err := local.NewStore(storeDir)
|
||||
require.NoError(t, err)
|
||||
err = content.WriteBlob(ctx, store, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
|
||||
ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
|
||||
require.NoError(t, err)
|
||||
|
||||
csID := "git-bundle-oci-store"
|
||||
bundleLocator := fmt.Sprintf("oci-layout+blob://not/real@%s", bundleDgst.String())
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
ref string // value passed as the fragment/ref to llb.Git
|
||||
}{
|
||||
{"sha-ref", headSha},
|
||||
{"symbolic-ref", "master"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
st := llb.Git(
|
||||
repoURL,
|
||||
tc.ref,
|
||||
llb.GitChecksum(headSha),
|
||||
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello\n", string(dt))
|
||||
})
|
||||
}
|
||||
|
||||
// KeepGitDir + bundle: the checkout's .git must not contain any leaked
|
||||
// refs/buildkit/* namespace. `.git/refs/heads/*` and `.git/refs/tags/*`
|
||||
// are populated by the plain-git checkout path (it runs its own fetch
|
||||
// into a freshly inited repo), so we only assert no refs/buildkit/
|
||||
// leak — the whole point of Item A is that the bundle no longer uses
|
||||
// that namespace.
|
||||
t.Run("keepgitdir-no-namespace-leak", func(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
st := llb.Git(
|
||||
repoURL,
|
||||
"master",
|
||||
llb.GitChecksum(headSha),
|
||||
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
|
||||
llb.KeepGitDir(),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// .git dir should be present.
|
||||
st2, err := os.Stat(filepath.Join(destDir, ".git"))
|
||||
require.NoError(t, err)
|
||||
require.True(t, st2.IsDir(), ".git should be a directory")
|
||||
|
||||
// No fake internal namespace anywhere under the checkout's .git.
|
||||
leakRoot := filepath.Join(destDir, ".git", "refs", "buildkit")
|
||||
_, err = os.Stat(leakRoot)
|
||||
require.True(t, os.IsNotExist(err), ".git/refs/buildkit should not exist: %v", err)
|
||||
|
||||
// Also scan packed-refs, which is where cloned/fetched refs may
|
||||
// live on disk after git gc / pack-refs.
|
||||
if pr, err := os.ReadFile(filepath.Join(destDir, ".git", "packed-refs")); err == nil {
|
||||
require.NotContains(t, string(pr), "refs/buildkit/",
|
||||
"packed-refs should not carry refs/buildkit/* entries")
|
||||
}
|
||||
|
||||
// The user's ref name must survive into .git: passing "master" to
|
||||
// llb.Git should produce a checkout whose .git carries
|
||||
// refs/heads/master, either loose or packed.
|
||||
//nolint:gosec // Test-controlled export dir is not attacker input.
|
||||
cmd := exec.CommandContext(context.TODO(), "git",
|
||||
"--git-dir="+filepath.Join(destDir, ".git"),
|
||||
"rev-parse", "--verify", "refs/heads/master")
|
||||
refOut, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, ".git should carry refs/heads/master; output=%q", string(refOut))
|
||||
require.Equal(t, headSha, strings.TrimSpace(string(refOut)))
|
||||
|
||||
// HEAD must point at the pinned commit.
|
||||
//nolint:gosec // Test-controlled export dir is not attacker input.
|
||||
cmd = exec.CommandContext(context.TODO(), "git",
|
||||
"--git-dir="+filepath.Join(destDir, ".git"),
|
||||
"rev-parse", "HEAD")
|
||||
gotOut, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "output=%q", string(gotOut))
|
||||
require.Equal(t, headSha, strings.TrimSpace(string(gotOut)))
|
||||
|
||||
// file.txt content matches.
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "file.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello\n", string(dt))
|
||||
})
|
||||
|
||||
// Provenance subtest: assert that the git-commit material and the
|
||||
// bundle-blob material are both recorded.
|
||||
t.Run("provenance", func(t *testing.T) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
|
||||
destDir := t.TempDir()
|
||||
st := llb.Scratch().File(
|
||||
llb.Copy(
|
||||
llb.Git(
|
||||
repoURL,
|
||||
"master",
|
||||
llb.GitChecksum(headSha),
|
||||
llb.GitBundleURL(bundleLocator, llb.GitBundleOCIStore("", csID)),
|
||||
),
|
||||
"file.txt",
|
||||
"file.txt",
|
||||
),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"attest:provenance": "",
|
||||
},
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var stmt struct {
|
||||
intoto.StatementHeader
|
||||
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(provDt, &stmt))
|
||||
|
||||
// Bundle-backed git sources emit two materials: the git source
|
||||
// at its raw repository URL (same shape as a non-bundle git
|
||||
// source), and the bundle blob as a purl with a vcs_url
|
||||
// qualifier linking back to the git URL.
|
||||
expectedGitURI := repoURL + "#master"
|
||||
var foundCommit, foundBundle bool
|
||||
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
|
||||
if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
|
||||
foundCommit = true
|
||||
}
|
||||
if strings.HasPrefix(m.URI, "pkg:oci/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
|
||||
foundBundle = true
|
||||
}
|
||||
}
|
||||
require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
require.True(t, foundBundle, "expected bundle blob material (pkg:oci/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
})
|
||||
|
||||
checkAllReleasable(t, c, sb, false)
|
||||
}
|
||||
|
||||
// testGitBundleRoundTripRegistry mirrors testGitBundleRoundTrip but resolves
|
||||
// the bundle blob through a registry using the docker-image+blob:// scheme.
|
||||
// The bundle bytes are uploaded directly to the sandbox registry by digest
|
||||
// and then re-imported in Build 2.
|
||||
func testGitBundleRoundTripRegistry(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
requiresLinux(t)
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
gitDir := t.TempDir()
|
||||
err = runInDir(gitDir,
|
||||
"git init -b master",
|
||||
"git config --local user.email test@example.com",
|
||||
"git config --local user.name test",
|
||||
"echo hello > file.txt",
|
||||
"git add file.txt",
|
||||
"git commit -m initial",
|
||||
"git update-server-info",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD")
|
||||
cmd.Dir = gitDir
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err)
|
||||
headSha := strings.TrimSpace(string(out))
|
||||
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
|
||||
defer server.Close()
|
||||
repoURL := server.URL + "/.git"
|
||||
|
||||
// Build 1: export a bundle via GitCheckoutBundle.
|
||||
bundleDir := t.TempDir()
|
||||
st := llb.Git(repoURL, "master", llb.GitChecksum(headSha), llb.GitCheckoutBundle())
|
||||
def, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: bundleDir}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundleBytes, err := os.ReadFile(filepath.Join(bundleDir, "bundle"))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, bundleBytes)
|
||||
bundleDgst := digest.FromBytes(bundleBytes)
|
||||
|
||||
// Push the bundle as a raw blob into the sandbox registry under a
|
||||
// repository name. The docker registry accepts arbitrary blobs by
|
||||
// digest; the repository exists implicitly once any blob is uploaded
|
||||
// under its name.
|
||||
bundleRepoRef := registry + "/foo/bundle@" + bundleDgst.String()
|
||||
ingester, err := contentutil.IngesterFromRef(bundleRepoRef)
|
||||
require.NoError(t, err)
|
||||
err = content.WriteBlob(ctx, ingester, "bundle-"+bundleDgst.String(), bytes.NewReader(bundleBytes),
|
||||
ocispecs.Descriptor{Digest: bundleDgst, Size: int64(len(bundleBytes))})
|
||||
require.NoError(t, err)
|
||||
|
||||
bundleLocator := "docker-image+blob://" + registry + "/foo/bundle@" + bundleDgst.String()
|
||||
|
||||
t.Run("checkout", func(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
st := llb.Git(
|
||||
repoURL,
|
||||
"master",
|
||||
llb.GitChecksum(headSha),
|
||||
llb.GitBundleURL(bundleLocator),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
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, "file.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello\n", string(dt))
|
||||
})
|
||||
|
||||
t.Run("provenance", func(t *testing.T) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureProvenance)
|
||||
destDir := t.TempDir()
|
||||
st := llb.Scratch().File(
|
||||
llb.Copy(
|
||||
llb.Git(
|
||||
repoURL,
|
||||
"master",
|
||||
llb.GitChecksum(headSha),
|
||||
llb.GitBundleURL(bundleLocator),
|
||||
),
|
||||
"file.txt",
|
||||
"file.txt",
|
||||
),
|
||||
)
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"attest:provenance": "",
|
||||
},
|
||||
Exports: []ExportEntry{{Type: ExporterLocal, OutputDir: destDir}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var stmt struct {
|
||||
intoto.StatementHeader
|
||||
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(provDt, &stmt))
|
||||
|
||||
expectedGitURI := repoURL + "#master"
|
||||
var foundCommit, foundBundle bool
|
||||
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
|
||||
if m.URI == expectedGitURI && m.Digest["sha1"] == headSha {
|
||||
foundCommit = true
|
||||
}
|
||||
if strings.HasPrefix(m.URI, "pkg:docker/") && strings.Contains(m.URI, "ref_type=bundle") && strings.Contains(m.URI, "vcs_url=") && m.Digest["sha256"] == bundleDgst.Hex() {
|
||||
foundBundle = true
|
||||
}
|
||||
}
|
||||
require.True(t, foundCommit, "expected git commit material %q with sha1=%s in %+v", expectedGitURI, headSha, stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
require.True(t, foundBundle, "expected bundle blob material (pkg:docker/...?ref_type=bundle) with sha256=%s in %+v", bundleDgst.Hex(), stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
})
|
||||
|
||||
checkAllReleasable(t, c, sb, false)
|
||||
}
|
||||
|
||||
func testGitResolveMutatedSource(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
gitDir := t.TempDir()
|
||||
gitCommands := []string{
|
||||
"git init",
|
||||
"git config --local user.email test",
|
||||
"git config --local user.name test",
|
||||
"echo a > a",
|
||||
"git add a",
|
||||
"git commit -m a",
|
||||
"git tag -a v0.1 -m v0.1",
|
||||
"echo b > b",
|
||||
"git add b",
|
||||
"git commit -m b",
|
||||
"git checkout -B v2",
|
||||
"git update-server-info",
|
||||
}
|
||||
err = runInDir(gitDir, gitCommands...)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "v0.1")
|
||||
cmd.Dir = gitDir
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err)
|
||||
commitTag := strings.TrimSpace(string(out))
|
||||
|
||||
cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v0.1^{commit}")
|
||||
cmd.Dir = gitDir
|
||||
out, err = cmd.Output()
|
||||
require.NoError(t, err)
|
||||
commitTagCommit := strings.TrimSpace(string(out))
|
||||
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
|
||||
defer server.Close()
|
||||
|
||||
dest := t.TempDir()
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: dest,
|
||||
},
|
||||
},
|
||||
}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git#v0.1"
|
||||
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
Attrs: map[string]string{
|
||||
"git.fullurl": server.URL + "/.git",
|
||||
},
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.Git)
|
||||
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
|
||||
require.Equal(t, commitTag, md.Git.Checksum)
|
||||
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
|
||||
|
||||
// update the tag to point to a different commit
|
||||
err = runInDir(gitDir, []string{
|
||||
"git tag -f v0.1",
|
||||
"git update-server-info",
|
||||
}...)
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
Attrs: map[string]string{
|
||||
"git.fullurl": server.URL + "/.git",
|
||||
},
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.Git)
|
||||
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
|
||||
require.Equal(t, commitTag, md.Git.Checksum)
|
||||
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
|
||||
|
||||
st := llb.Git(server.URL+"/.git", "", llb.GitRef("v0.1"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(dest, "b"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err), "expected file b to not exist")
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dest, "a"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "a\n", string(dt))
|
||||
|
||||
checkAllReleasable(t, c, sb, false)
|
||||
}
|
||||
|
||||
func testGitResolveSourceMetadata(t *testing.T, sb integration.Sandbox) {
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
gitDir := t.TempDir()
|
||||
gitCommands := []string{
|
||||
"git init",
|
||||
"git config --local user.email test@example.com",
|
||||
"git config --local user.name test",
|
||||
"touch a",
|
||||
"git add a",
|
||||
"git commit -m msg",
|
||||
"git tag -a v0.1 -m v0.1release",
|
||||
"echo b > b",
|
||||
"git add b",
|
||||
"git commit -m b",
|
||||
"git checkout -B v2",
|
||||
"git update-server-info",
|
||||
}
|
||||
err = runInDir(gitDir, gitCommands...)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.CommandContext(context.TODO(), "git", "rev-parse", "HEAD")
|
||||
cmd.Dir = gitDir
|
||||
out, err := cmd.Output()
|
||||
require.NoError(t, err)
|
||||
commitHEAD := strings.TrimSpace(string(out))
|
||||
|
||||
cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v0.1")
|
||||
cmd.Dir = gitDir
|
||||
out, err = cmd.Output()
|
||||
require.NoError(t, err)
|
||||
commitTag := strings.TrimSpace(string(out))
|
||||
|
||||
cmd = exec.CommandContext(context.TODO(), "git", "rev-parse", "v0.1^{commit}")
|
||||
cmd.Dir = gitDir
|
||||
out, err = cmd.Output()
|
||||
require.NoError(t, err)
|
||||
commitTagCommit := strings.TrimSpace(string(out))
|
||||
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
|
||||
defer server.Close()
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
id := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git"
|
||||
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
Attrs: map[string]string{
|
||||
"git.fullurl": server.URL + "/.git",
|
||||
},
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.Git)
|
||||
require.Equal(t, "refs/heads/v2", md.Git.Ref) // default to branch head
|
||||
require.Equal(t, commitHEAD, md.Git.Checksum)
|
||||
require.Equal(t, "", md.Git.CommitChecksum) // not annotated tag
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
|
||||
require.Nil(t, md.Git.CommitObject)
|
||||
require.Nil(t, md.Git.TagObject)
|
||||
|
||||
id += "#v0.1"
|
||||
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
Attrs: map[string]string{
|
||||
"git.fullurl": server.URL + "/.git",
|
||||
},
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.Git)
|
||||
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
|
||||
require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
|
||||
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
|
||||
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
|
||||
require.Nil(t, md.Git.CommitObject)
|
||||
require.Nil(t, md.Git.TagObject)
|
||||
|
||||
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
Attrs: map[string]string{
|
||||
"git.fullurl": server.URL + "/.git",
|
||||
},
|
||||
}, sourceresolver.Opt{
|
||||
GitOpt: &sourceresolver.ResolveGitOpt{
|
||||
ReturnObject: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.Git)
|
||||
require.Equal(t, "refs/tags/v0.1", md.Git.Ref)
|
||||
require.Equal(t, commitTag, md.Git.Checksum) // annotated tag
|
||||
require.Equal(t, commitTagCommit, md.Git.CommitChecksum)
|
||||
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
require.Equal(t, server.URL+"/.git", md.Op.Attrs["git.fullurl"])
|
||||
require.NotNil(t, md.Git.CommitObject)
|
||||
require.NotNil(t, md.Git.TagObject)
|
||||
|
||||
commitObj, err := gitobject.Parse(md.Git.CommitObject)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, commitObj.VerifyChecksum(md.Git.CommitChecksum))
|
||||
|
||||
commit, err := commitObj.ToCommit()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "msg", commit.Message)
|
||||
require.Equal(t, "test", commit.Author.Name)
|
||||
require.Equal(t, "test@example.com", commit.Author.Email)
|
||||
require.Equal(t, "test", commit.Committer.Name)
|
||||
require.Equal(t, "test@example.com", commit.Committer.Email)
|
||||
commitTime := commit.Committer.When
|
||||
require.NotNil(t, commitTime)
|
||||
require.WithinDuration(t, time.Now(), *commitTime, 2*time.Minute)
|
||||
|
||||
tagObj, err := gitobject.Parse(md.Git.TagObject)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, tagObj.VerifyChecksum(md.Git.Checksum))
|
||||
|
||||
tag, err := tagObj.ToTag()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "v0.1release", tag.Message)
|
||||
require.Equal(t, "v0.1", tag.Tag)
|
||||
require.Equal(t, "test", tag.Tagger.Name)
|
||||
require.Equal(t, "test@example.com", tag.Tagger.Email)
|
||||
tagTime := tag.Tagger.When
|
||||
require.NotNil(t, tagTime)
|
||||
require.WithinDuration(t, time.Now(), *tagTime, 2*time.Minute)
|
||||
return nil, nil
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func runInDir(dir string, cmds ...string) error {
|
||||
return runInDirEnv(dir, nil, cmds...)
|
||||
}
|
||||
|
||||
func runInDirEnv(dir string, env []string, cmds ...string) error {
|
||||
for _, args := range cmds {
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.CommandContext(context.TODO(), "powershell", "-command", args)
|
||||
} else {
|
||||
cmd = exec.CommandContext(context.TODO(), "sh", "-c", args)
|
||||
}
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
cmd.Dir = dir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrapf(err, "error running %v", args)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/session/secrets/secretsprovider"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/testutil/httpserver"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testBuildHTTPSource(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// invalid URL first
|
||||
st := llb.HTTP(server.URL + "/bar")
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid response status 404")
|
||||
|
||||
// first correct request
|
||||
st = llb.HTTP(server.URL + "/foo")
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, server.Stats("/foo").AllRequests)
|
||||
require.Equal(t, 0, server.Stats("/foo").CachedRequests)
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, server.Stats("/foo").AllRequests)
|
||||
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(tmpdir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("content1"), dt)
|
||||
|
||||
allReqs := server.Stats("/foo").Requests
|
||||
require.Equal(t, 2, len(allReqs))
|
||||
require.Equal(t, http.MethodGet, allReqs[0].Method)
|
||||
require.Equal(t, "gzip", allReqs[0].Header.Get("Accept-Encoding"))
|
||||
require.Equal(t, http.MethodHead, allReqs[1].Method)
|
||||
require.Equal(t, "gzip", allReqs[1].Header.Get("Accept-Encoding"))
|
||||
|
||||
require.NoError(t, os.RemoveAll(filepath.Join(tmpdir, "foo")))
|
||||
|
||||
// update the content at the url to be gzipped now, the final output
|
||||
// should remain the same
|
||||
modTime = time.Now().Add(-23 * time.Hour)
|
||||
var buf bytes.Buffer
|
||||
gw := gzip.NewWriter(&buf)
|
||||
_, err = gw.Write(resp.Content)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gw.Close())
|
||||
gzipBytes := buf.Bytes()
|
||||
respGzip := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: gzipBytes,
|
||||
LastModified: &modTime,
|
||||
ContentEncoding: "gzip",
|
||||
}
|
||||
server.SetRoute("/foo", respGzip)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 4, server.Stats("/foo").AllRequests)
|
||||
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(tmpdir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.Content, dt)
|
||||
|
||||
allReqs = server.Stats("/foo").Requests
|
||||
require.Equal(t, 4, len(allReqs))
|
||||
require.Equal(t, http.MethodHead, allReqs[2].Method)
|
||||
require.Equal(t, "gzip", allReqs[2].Header.Get("Accept-Encoding"))
|
||||
require.Equal(t, http.MethodGet, allReqs[3].Method)
|
||||
require.Equal(t, "gzip", allReqs[3].Header.Get("Accept-Encoding"))
|
||||
|
||||
// test extra options
|
||||
// llb.Chown not supported on Windows
|
||||
st = integration.UnixOrWindows(
|
||||
llb.HTTP(server.URL+"/foo", llb.Filename("bar"), llb.Chmod(0741), llb.Chown(1000, 1000)),
|
||||
llb.HTTP(server.URL+"/foo", llb.Filename("bar"), llb.Chmod(0741)),
|
||||
)
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 5, server.Stats("/foo").AllRequests)
|
||||
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(tmpdir, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("content1"), dt)
|
||||
|
||||
fi, err := os.Stat(filepath.Join(tmpdir, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fi.ModTime().Format(http.TimeFormat), modTime.Format(http.TimeFormat))
|
||||
|
||||
// no support for llb.Chmod on Windows, default is returned
|
||||
fMode := integration.UnixOrWindows(0741, 0666)
|
||||
require.Equal(t, fMode, int(fi.Mode()&0777))
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
// TODO: check that second request was marked as cached
|
||||
}
|
||||
|
||||
func testBuildHTTPSourceAuthHeaderSecret(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid false positive with current time
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
st := llb.HTTP(server.URL+"/foo", llb.AuthHeaderSecret("http-secret"))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(
|
||||
sb.Context(),
|
||||
def,
|
||||
SolveOpt{
|
||||
Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{
|
||||
"http-secret": []byte("Bearer foo"),
|
||||
})},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
allReqs := server.Stats("/foo").Requests
|
||||
require.Equal(t, 1, len(allReqs))
|
||||
require.Equal(t, http.MethodGet, allReqs[0].Method)
|
||||
require.Equal(t, "Bearer foo", allReqs[0].Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// docker/buildx#2803
|
||||
func testBuildHTTPSourceEtagScope(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
|
||||
|
||||
sharedEtag := identity.NewID()
|
||||
resp := &httpserver.Response{
|
||||
Etag: sharedEtag,
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
resp2 := &httpserver.Response{
|
||||
Etag: sharedEtag,
|
||||
Content: []byte("another"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/one/foo": resp,
|
||||
"/two/foo": resp2,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// first correct request
|
||||
st := llb.HTTP(server.URL + "/one/foo")
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
out1 := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: out1,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, server.Stats("/one/foo").AllRequests)
|
||||
require.Equal(t, 0, server.Stats("/one/foo").CachedRequests)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(out1, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("content1"), dt)
|
||||
|
||||
st = llb.HTTP(server.URL + "/two/foo")
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
out2 := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: out2,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, server.Stats("/two/foo").AllRequests)
|
||||
require.Equal(t, 0, server.Stats("/two/foo").CachedRequests)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(out2, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("another"), dt)
|
||||
|
||||
out2 = t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: out2,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, server.Stats("/two/foo").AllRequests)
|
||||
require.Equal(t, 1, server.Stats("/two/foo").CachedRequests)
|
||||
|
||||
allReqs := server.Stats("/two/foo").Requests
|
||||
require.Equal(t, 2, len(allReqs))
|
||||
require.Equal(t, http.MethodGet, allReqs[0].Method)
|
||||
require.Equal(t, "gzip", allReqs[0].Header.Get("Accept-Encoding"))
|
||||
require.Equal(t, http.MethodHead, allReqs[1].Method)
|
||||
require.Equal(t, "gzip", allReqs[1].Header.Get("Accept-Encoding"))
|
||||
|
||||
require.NoError(t, os.RemoveAll(filepath.Join(out2, "foo")))
|
||||
}
|
||||
|
||||
func testBuildHTTPSourceHeader(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
st := llb.HTTP(
|
||||
server.URL+"/foo",
|
||||
llb.Header(llb.HTTPHeader{
|
||||
Accept: "application/vnd.foo",
|
||||
UserAgent: "fooagent",
|
||||
}),
|
||||
)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
allReqs := server.Stats("/foo").Requests
|
||||
require.Equal(t, 1, len(allReqs))
|
||||
require.Equal(t, http.MethodGet, allReqs[0].Method)
|
||||
require.Equal(t, "application/vnd.foo", allReqs[0].Header.Get("accept"))
|
||||
require.Equal(t, "fooagent", allReqs[0].Header.Get("user-agent"))
|
||||
}
|
||||
|
||||
func testBuildHTTPSourceHostTokenSecret(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid false positive with current time
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
st := llb.HTTP(server.URL + "/foo")
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(
|
||||
sb.Context(),
|
||||
def,
|
||||
SolveOpt{
|
||||
Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{
|
||||
"HTTP_AUTH_TOKEN_127.0.0.1": []byte("123456"),
|
||||
})},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
allReqs := server.Stats("/foo").Requests
|
||||
require.Equal(t, 1, len(allReqs))
|
||||
require.Equal(t, http.MethodGet, allReqs[0].Method)
|
||||
require.Equal(t, "Bearer 123456", allReqs[0].Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func testBuildHTTPSourcePGPSignatureVerify(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
signFixturesPath, ok := os.LookupEnv("BUILDKIT_TEST_SIGN_FIXTURES")
|
||||
if !ok {
|
||||
t.Skip("missing BUILDKIT_TEST_SIGN_FIXTURES")
|
||||
}
|
||||
|
||||
payload, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact"))
|
||||
require.NoError(t, err)
|
||||
sigData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact.asc"))
|
||||
require.NoError(t, err)
|
||||
pubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.gpg.pub"))
|
||||
require.NoError(t, err)
|
||||
wrongPubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user2.gpg.pub"))
|
||||
require.NoError(t, err)
|
||||
|
||||
httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/artifact.txt" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(payload)
|
||||
}))
|
||||
defer httpSrv.Close()
|
||||
|
||||
solve := func(t *testing.T, state llb.State) error {
|
||||
def, err := state.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
tmpdir := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dt, err := os.ReadFile(filepath.Join(tmpdir, "artifact.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, payload, dt)
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("valid-signature", func(t *testing.T) {
|
||||
validState := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: pubKeyData,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, solve(t, validState))
|
||||
})
|
||||
|
||||
t.Run("wrong-pubkey", func(t *testing.T) {
|
||||
invalidState := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: wrongPubKeyData,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
err = solve(t, invalidState)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to verify pgp signature")
|
||||
})
|
||||
|
||||
t.Run("concatenated-pubkeys-right-key-second", func(t *testing.T) {
|
||||
mergedPubKeys := append(append([]byte{}, wrongPubKeyData...), '\n')
|
||||
mergedPubKeys = append(mergedPubKeys, pubKeyData...)
|
||||
state := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: mergedPubKeys,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, solve(t, state))
|
||||
})
|
||||
}
|
||||
|
||||
func testBuildHTTPSourceUnauthorizedChecksumRace(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureMergeDiff)
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
var requests atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if requests.Add(1) == 1 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
url := server.URL + "/test.txt"
|
||||
lower := llb.Scratch()
|
||||
// Reproduce the race between same-URL HTTP sources with and without
|
||||
// checksum sharing the resolver cache while the server returns 401.
|
||||
withoutChecksum := lower.File(llb.Copy(llb.HTTP(url), "test.txt", "/a.txt"))
|
||||
withChecksum := lower.File(llb.Copy(llb.HTTP(url, llb.Checksum(digest.FromBytes(nil))), "test.txt", "/b.txt"))
|
||||
st := llb.Merge([]llb.State{
|
||||
llb.Diff(lower, withoutChecksum),
|
||||
llb.Diff(lower, withChecksum),
|
||||
})
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
for range 10 {
|
||||
requests.Store(0)
|
||||
_, err := c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: t.TempDir(),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid response status 401")
|
||||
}
|
||||
}
|
||||
|
||||
func testHTTPPruneAfterCacheKey(t *testing.T, sb integration.Sandbox) {
|
||||
// this test depends on hitting race condition in internal functions.
|
||||
// If debugging and expecting failure you can add small sleep in beginning of source/http.Exec() to hit reliably
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
startScan := make(chan struct{})
|
||||
stopScan := make(chan struct{})
|
||||
pauseScan := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
// attempt to prune the HTTP record in between cachekey and snapshot
|
||||
defer close(done)
|
||||
for {
|
||||
select {
|
||||
case <-startScan:
|
||||
scan:
|
||||
for {
|
||||
select {
|
||||
case <-pauseScan:
|
||||
break scan
|
||||
default:
|
||||
du, err := c.DiskUsage(ctx)
|
||||
assert.NoError(t, err)
|
||||
for _, entry := range du {
|
||||
if entry.Description == "http url "+server.URL+"/foo" {
|
||||
if !entry.InUse {
|
||||
t.Logf("entry no longer in use, pruning")
|
||||
err = c.Prune(ctx, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp.Etag = identity.NewID()
|
||||
resp.Content = []byte("content2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-stopScan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
const iterations = 10
|
||||
for range iterations {
|
||||
startScan <- struct{}{}
|
||||
resp.Etag = identity.NewID()
|
||||
resp.Content = []byte("content1")
|
||||
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
st := llb.Scratch().File(llb.Copy(llb.HTTP(server.URL+"/foo"), "foo", "bar"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
pauseScan <- struct{}{}
|
||||
|
||||
err = c.Prune(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, false)
|
||||
}
|
||||
close(stopScan)
|
||||
<-done
|
||||
}
|
||||
|
||||
// testHTTPPruneAfterResolveMeta ensures that pruning after ResolveSourceMetadata
|
||||
// doesn't pull in new data for same build. Once URL has been resolved once for a specific
|
||||
// build, the data should be considered immutable and remote changes don't affect ongoing build.
|
||||
func testHTTPPruneAfterResolveMeta(t *testing.T, sb integration.Sandbox) {
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dest := t.TempDir()
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: dest,
|
||||
},
|
||||
},
|
||||
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
|
||||
id := server.URL + "/foo"
|
||||
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
|
||||
// prune all
|
||||
err = c.Prune(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp.Content = []byte("content2") // etag is same so should hit cache if record not pruned
|
||||
|
||||
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gc.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "content1", string(dt))
|
||||
|
||||
checkAllReleasable(t, c, sb, false)
|
||||
}
|
||||
|
||||
func testHTTPResolveMetaReuse(t *testing.T, sb integration.Sandbox) {
|
||||
// the difference with testHTTPPruneAfterResolveMeta is that here we change content with the etag on the server
|
||||
// but because the URL was already resolved once, the new content should not be seen
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dest := t.TempDir()
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: dest,
|
||||
},
|
||||
},
|
||||
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
|
||||
id := server.URL + "/foo"
|
||||
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
|
||||
resp.Etag = identity.NewID()
|
||||
resp.Content = []byte("content2") // etag changed so new content would be returned if re-resolving
|
||||
|
||||
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gc.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "content1", string(dt))
|
||||
}
|
||||
|
||||
// testHTTPResolveMultiBuild is a negative test for testHTTPResolveMetaReuse to ensure that
|
||||
// URLs are resolved in between separate builds
|
||||
func testHTTPResolveMultiBuild(t *testing.T, sb integration.Sandbox) {
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
}
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dest := t.TempDir()
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: dest,
|
||||
},
|
||||
},
|
||||
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
|
||||
id := server.URL + "/foo"
|
||||
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
|
||||
|
||||
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gc.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "content1", string(dt))
|
||||
|
||||
resp.Etag = identity.NewID()
|
||||
resp.Content = []byte("content2")
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: dest,
|
||||
},
|
||||
},
|
||||
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
|
||||
id := server.URL + "/foo"
|
||||
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
|
||||
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gc.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(dest, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "content2", string(dt))
|
||||
}
|
||||
|
||||
func testHTTPResolveSourceMetadata(t *testing.T, sb integration.Sandbox) {
|
||||
ctx := sb.Context()
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
|
||||
|
||||
resp := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content1"),
|
||||
LastModified: &modTime,
|
||||
}
|
||||
|
||||
resp2 := &httpserver.Response{
|
||||
Etag: identity.NewID(),
|
||||
Content: []byte("content2"),
|
||||
ContentDisposition: "attachment; filename=\"my img.jpg\"",
|
||||
}
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/foo": resp,
|
||||
"/bar": resp2,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
id := server.URL + "/foo"
|
||||
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
|
||||
require.Equal(t, "foo", md.HTTP.Filename)
|
||||
require.NotNil(t, md.HTTP.LastModified)
|
||||
require.Equal(t, modTime.Unix(), md.HTTP.LastModified.Unix())
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
|
||||
id = server.URL + "/bar"
|
||||
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
|
||||
Identifier: id,
|
||||
}, sourceresolver.Opt{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.NotNil(t, md.HTTP)
|
||||
require.Equal(t, digest.FromBytes(resp2.Content), md.HTTP.Digest)
|
||||
require.Equal(t, "my img.jpg", md.HTTP.Filename)
|
||||
require.Nil(t, md.HTTP.LastModified)
|
||||
require.Equal(t, id, md.Op.Identifier)
|
||||
return nil, nil
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/session"
|
||||
sessionauth "github.com/moby/buildkit/session/auth"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func testClientGatewayCanceledCredentialsCallbackReturns(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
username := "buildkit-user"
|
||||
password := "buildkit-pass"
|
||||
repo := "buildkit/auth-session-" + identity.NewID()
|
||||
backendRef := registry + "/" + repo + ":latest"
|
||||
|
||||
st := llb.Scratch().File(llb.Mkfile("hello", 0o644, []byte("world")))
|
||||
def, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": backendRef,
|
||||
"push": "true",
|
||||
},
|
||||
}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
target, err := url.Parse("http://" + registry)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
director := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
director(req)
|
||||
req.Host = target.Host
|
||||
}
|
||||
|
||||
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, secret, ok := r.BasicAuth()
|
||||
if !ok || user != username || secret != password {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="buildkit-test"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
}))
|
||||
t.Cleanup(proxyServer.Close)
|
||||
|
||||
ref := strings.TrimPrefix(proxyServer.URL, "http://") + "/" + repo + ":latest"
|
||||
host, _, _ := strings.Cut(ref, "/")
|
||||
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var startedOnce sync.Once
|
||||
|
||||
provider := &blockingAuthProvider{
|
||||
t: t,
|
||||
host: host,
|
||||
username: username,
|
||||
password: password,
|
||||
started: started,
|
||||
startedOnce: &startedOnce,
|
||||
release: release,
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Session: []session.Attachable{provider},
|
||||
}, "buildkit_test", func(ctx context.Context, gw client.Client) (*client.Result, error) {
|
||||
reqCtx, cancel := context.WithCancelCause(ctx)
|
||||
defer cancel(context.Canceled)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
_, _, _, err := gw.ResolveImageConfig(reqCtx, ref, sourceresolver.Opt{})
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(15 * time.Second):
|
||||
return nil, errors.New("timed out waiting for registry credential callback")
|
||||
}
|
||||
|
||||
cancel(context.Canceled)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.Error(t, err)
|
||||
return client.NewResult(), nil
|
||||
case <-time.After(3 * time.Second):
|
||||
close(release)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.Error(t, err)
|
||||
case <-time.After(15 * time.Second):
|
||||
return nil, errors.New("timed out draining canceled image config resolution")
|
||||
}
|
||||
|
||||
return nil, errors.New("canceled image config resolution stayed blocked until the credentials callback was released")
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testPullWithLayerLimit(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Scratch().
|
||||
File(llb.Mkfile("/first", 0644, []byte("first"))).
|
||||
File(llb.Mkfile("/second", 0644, []byte("second"))).
|
||||
File(llb.Mkfile("/third", 0644, []byte("third")))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
target := registry + "/buildkit/testlayers:latest"
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// pull 2 first layers
|
||||
st = llb.Image(target, llb.WithLayerLimit(2)).
|
||||
File(llb.Mkfile("/forth", 0644, []byte("forth")))
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "first"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "first", string(dt))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "second"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "second", string(dt))
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "third"))
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, os.ErrNotExist))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "forth"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "forth", string(dt))
|
||||
|
||||
// pull 3rd layer only
|
||||
st = llb.Diff(
|
||||
llb.Image(target, llb.WithLayerLimit(2)),
|
||||
llb.Image(target)).
|
||||
File(llb.Mkfile("/forth", 0644, []byte("forth")))
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
}},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "first"))
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, os.ErrNotExist))
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "second"))
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, os.ErrNotExist))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "third"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "third", string(dt))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "forth"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "forth", string(dt))
|
||||
|
||||
// zero limit errors cleanly
|
||||
st = llb.Image(target, llb.WithLayerLimit(0))
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid layer limit")
|
||||
}
|
||||
|
||||
func testValidateDigestOrigin(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
||||
scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
||||
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
"touch foo",
|
||||
"cmd /C echo a > foo",
|
||||
)
|
||||
st := llb.Image(imgName).
|
||||
Run(llb.Shlex(cmdStr), llb.Dir("/wd")).
|
||||
AddMount("/wd", scratch)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
target := registry + "/buildkit/testdigest:latest"
|
||||
|
||||
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
||||
require.True(t, ok)
|
||||
|
||||
err = c.Prune(sb.Context(), nil, PruneAll)
|
||||
require.NoError(t, err)
|
||||
|
||||
st = llb.Image(target + "@" + dgst)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// accessing the digest from invalid names should fail
|
||||
st = llb.Image("example.invalid/nosuchrepo@" + dgst)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// also check repo that does exists but not digest
|
||||
st = llb.Image("docker.io/library/ubuntu@" + dgst)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
type blockingAuthProvider struct {
|
||||
sessionauth.UnimplementedAuthServer
|
||||
|
||||
t *testing.T
|
||||
host string
|
||||
username string
|
||||
password string
|
||||
started chan struct{}
|
||||
startedOnce *sync.Once
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (p *blockingAuthProvider) Register(server *grpc.Server) {
|
||||
sessionauth.RegisterAuthServer(server, p)
|
||||
}
|
||||
|
||||
func (p *blockingAuthProvider) Credentials(ctx context.Context, req *sessionauth.CredentialsRequest) (*sessionauth.CredentialsResponse, error) {
|
||||
require.Equal(p.t, p.host, req.Host)
|
||||
p.startedOnce.Do(func() {
|
||||
close(p.started)
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, context.Cause(ctx)
|
||||
case <-p.release:
|
||||
return &sessionauth.CredentialsResponse{
|
||||
Username: p.username,
|
||||
Secret: p.password,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func testLocalSourceDiffer(t *testing.T, sb integration.Sandbox) {
|
||||
for _, d := range []llb.DiffType{llb.DiffNone, llb.DiffMetadata} {
|
||||
t.Run(fmt.Sprintf("differ=%s", d), func(t *testing.T) {
|
||||
testLocalSourceWithDiffer(t, sb, d)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testLocalSourceWithDiffer(t *testing.T, sb integration.Sandbox, d llb.DiffType) {
|
||||
c, err := New(context.TODO(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("foo", []byte("foo"), 0600),
|
||||
)
|
||||
|
||||
tv := syscall.NsecToTimespec(time.Now().UnixNano())
|
||||
|
||||
err = syscall.UtimesNano(filepath.Join(dir.Name, "foo"), []syscall.Timespec{tv, tv})
|
||||
require.NoError(t, err)
|
||||
|
||||
st := llb.Local("mylocal"+string(d), llb.Differ(d, false))
|
||||
|
||||
def, err := st.Marshal(context.TODO())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(context.TODO(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal" + string(d): dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("foo"), dt)
|
||||
|
||||
// Give some extra time to ensure the resources from first solve are released
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir.Name, "foo"), []byte("bar"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = syscall.UtimesNano(filepath.Join(dir.Name, "foo"), []syscall.Timespec{tv, tv})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(context.TODO(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal" + string(d): dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
if d == llb.DiffMetadata {
|
||||
require.Equal(t, []byte("foo"), dt)
|
||||
}
|
||||
if d == llb.DiffNone {
|
||||
require.Equal(t, []byte("bar"), dt)
|
||||
}
|
||||
}
|
||||
|
||||
// moby/buildkit#4831
|
||||
func testLocalSourceWithHardlinksFilter(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(context.TODO(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("bar", []byte("bar"), 0600),
|
||||
fstest.Link("bar", "foo1"),
|
||||
fstest.Link("bar", "foo2"),
|
||||
)
|
||||
|
||||
st := llb.Local("mylocal", llb.FollowPaths([]string{"foo*"}))
|
||||
|
||||
def, err := st.Marshal(context.TODO())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(context.TODO(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "bar"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "foo1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("bar"), dt)
|
||||
|
||||
st1, err := os.Stat(filepath.Join(destDir, "foo1"))
|
||||
require.NoError(t, err)
|
||||
|
||||
st2, err := os.Stat(filepath.Join(destDir, "foo2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, os.SameFile(st1, st2))
|
||||
}
|
||||
|
||||
func testLocalSymlinkEscape(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
test := []byte(`set -ex
|
||||
[[ -L /mount/foo ]]
|
||||
[[ -L /mount/sub/bar ]]
|
||||
[[ -L /mount/bax ]]
|
||||
[[ -f /mount/bay ]]
|
||||
[[ -f /mount/sub/sub2/file ]]
|
||||
[[ ! -f /mount/baz ]]
|
||||
[[ ! -f /mount/etc/passwd ]]
|
||||
[[ ! -f /mount/etc/group ]]
|
||||
[[ $(readlink /mount/foo) == "/etc/passwd" ]]
|
||||
[[ $(readlink /mount/sub/bar) == "../../../etc/group" ]]
|
||||
`)
|
||||
|
||||
dir := integration.Tmpdir(
|
||||
t,
|
||||
// point to absolute path that is not part of dir
|
||||
fstest.Symlink("/etc/passwd", "foo"),
|
||||
fstest.CreateDir("sub", 0700),
|
||||
// point outside of the dir
|
||||
fstest.Symlink("../../../etc/group", "sub/bar"),
|
||||
// regular valid symlink
|
||||
fstest.Symlink("bay", "bax"),
|
||||
// target for symlink (not requested)
|
||||
fstest.CreateFile("bay", []byte{}, 0600),
|
||||
// file with many subdirs
|
||||
fstest.CreateDir("sub/sub2", 0700),
|
||||
fstest.CreateFile("sub/sub2/file", []byte{}, 0600),
|
||||
// unused file that shouldn't be included
|
||||
fstest.CreateFile("baz", []byte{}, 0600),
|
||||
fstest.CreateFile("test.sh", test, 0700),
|
||||
)
|
||||
|
||||
local := llb.Local("mylocal", llb.FollowPaths([]string{
|
||||
"test.sh", "foo", "sub/bar", "bax", "sub/sub2/file",
|
||||
}))
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh /mount/test.sh`), llb.AddMount("/mount", local, llb.Readonly))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": dir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testMetadataOnlyLocal(t *testing.T, sb integration.Sandbox) {
|
||||
ctx, cancel := context.WithCancelCause(sb.Context())
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
srcDir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("data", []byte("contents"), 0600),
|
||||
fstest.CreateDir("dir", 0700),
|
||||
fstest.CreateFile("dir/file1", []byte("file1"), 0600),
|
||||
fstest.CreateFile("dir/file2", []byte("file2"), 0600),
|
||||
fstest.CreateDir("dir/subdir", 0700),
|
||||
fstest.CreateDir("dir/subdir2", 0700),
|
||||
fstest.CreateFile("dir/subdir/bar1", []byte("bar1"), 0600),
|
||||
fstest.CreateFile("dir/subdir/bar2", []byte("bar2"), 0600),
|
||||
fstest.CreateFile("dir/subdir2/bar3", []byte("bar3"), 0600),
|
||||
fstest.CreateFile("foo", []byte("foo"), 0602),
|
||||
)
|
||||
|
||||
def, err := llb.Local("source", llb.MetadataOnlyTransfer([]string{"dir/**/*1", "foo"})).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"source": srcDir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "data"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
act, err := os.ReadFile(filepath.Join(destDir, "dir/file1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file1", string(act))
|
||||
|
||||
act, err = os.ReadFile(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "foo", string(act))
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "dir/file2"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
act, err = os.ReadFile(filepath.Join(destDir, "dir/subdir/bar1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bar1", string(act))
|
||||
|
||||
_, err = os.Stat(filepath.Join(destDir, "dir/subdir2"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "dir/subdir/bar2"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, ".fsutil-metadata"))
|
||||
require.NoError(t, err)
|
||||
|
||||
stats := parseFSMetadata(t, dt)
|
||||
require.Equal(t, 10, len(stats))
|
||||
|
||||
require.Equal(t, "data", stats[0].Path)
|
||||
require.Equal(t, "dir", stats[1].Path)
|
||||
require.Equal(t, "dir/file1", stats[2].Path)
|
||||
require.Equal(t, "dir/file2", stats[3].Path)
|
||||
require.Equal(t, "dir/subdir", stats[4].Path)
|
||||
require.Equal(t, "dir/subdir/bar1", stats[5].Path)
|
||||
require.Equal(t, "dir/subdir/bar2", stats[6].Path)
|
||||
require.Equal(t, "dir/subdir2", stats[7].Path)
|
||||
require.Equal(t, "dir/subdir2/bar3", stats[8].Path)
|
||||
require.Equal(t, "foo", stats[9].Path)
|
||||
|
||||
err = os.RemoveAll(filepath.Join(srcDir.Name, "dir/subdir"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcDir.Name, "dir/file1"), []byte("file1-updated"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcDir.Name, "dir/bar1"), []byte("bar1"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
def, err = llb.Local("source", llb.MetadataOnlyTransfer([]string{"dir/**/*1", "foo"})).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"source": srcDir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(destDir, "data"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
act, err = os.ReadFile(filepath.Join(destDir, "dir/file1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file1-updated", string(act))
|
||||
|
||||
act, err = os.ReadFile(filepath.Join(destDir, "dir/bar1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bar1", string(act))
|
||||
|
||||
_, err = os.Stat(filepath.Join(destDir, "dir/subdir"))
|
||||
require.Error(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, ".fsutil-metadata"))
|
||||
require.NoError(t, err)
|
||||
|
||||
stats = parseFSMetadata(t, dt)
|
||||
require.Equal(t, 8, len(stats))
|
||||
|
||||
require.Equal(t, "data", stats[0].Path)
|
||||
require.Equal(t, "dir", stats[1].Path)
|
||||
require.Equal(t, "dir/bar1", stats[2].Path)
|
||||
require.Equal(t, "dir/file1", stats[3].Path)
|
||||
require.Equal(t, "dir/file2", stats[4].Path)
|
||||
require.Equal(t, "dir/subdir2", stats[5].Path)
|
||||
require.Equal(t, "dir/subdir2/bar3", stats[6].Path)
|
||||
require.Equal(t, "foo", stats[7].Path)
|
||||
}
|
||||
|
||||
// moby/buildkit#492
|
||||
func testParallelLocalBuilds(t *testing.T, sb integration.Sandbox) {
|
||||
ctx, cancel := context.WithCancelCause(sb.Context())
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for i := range 3 {
|
||||
func(i int) {
|
||||
eg.Go(func() error {
|
||||
fn := fmt.Sprintf("test%d", i)
|
||||
srcDir := integration.Tmpdir(
|
||||
t,
|
||||
fstest.CreateFile(fn, []byte("contents"), 0600),
|
||||
)
|
||||
|
||||
def, err := llb.Local("source").Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"source": srcDir,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
act, err := os.ReadFile(filepath.Join(destDir, fn))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "contents", string(act))
|
||||
return nil
|
||||
})
|
||||
}(i)
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/continuity/fs/fstest"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testMergeOp(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureMergeDiff)
|
||||
requiresLinux(t)
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
ctx := sb.Context()
|
||||
registry, err := sb.NewRegistry()
|
||||
if !errors.Is(err, integration.ErrRequirements) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
var imageTarget string
|
||||
if registry != "" {
|
||||
imageTarget = registry + "/buildkit/testmergeop:latest"
|
||||
}
|
||||
|
||||
stateA := llb.Scratch().
|
||||
File(llb.Mkfile("/foo", 0755, []byte("A"))).
|
||||
File(llb.Mkfile("/a", 0755, []byte("A"))).
|
||||
File(llb.Mkdir("/bar", 0700)).
|
||||
File(llb.Mkfile("/bar/A", 0755, []byte("A")))
|
||||
stateB := stateA.
|
||||
File(llb.Rm("/foo")).
|
||||
File(llb.Mkfile("/b", 0755, []byte("B"))).
|
||||
File(llb.Mkfile("/bar/B", 0754, []byte("B")))
|
||||
stateC := llb.Scratch().
|
||||
File(llb.Mkfile("/foo", 0755, []byte("C"))).
|
||||
File(llb.Mkfile("/c", 0755, []byte("C"))).
|
||||
File(llb.Mkdir("/bar", 0755)).
|
||||
File(llb.Mkfile("/bar/A", 0400, []byte("C")))
|
||||
|
||||
mergeA := llb.Merge([]llb.State{stateA, stateC})
|
||||
requireContents(ctx, t, c, sb, mergeA, nil, nil, imageTarget,
|
||||
fstest.CreateFile("foo", []byte("C"), 0755),
|
||||
fstest.CreateFile("c", []byte("C"), 0755),
|
||||
fstest.CreateDir("bar", 0755),
|
||||
fstest.CreateFile("bar/A", []byte("C"), 0400),
|
||||
fstest.CreateFile("a", []byte("A"), 0755),
|
||||
)
|
||||
|
||||
mergeB := llb.Merge([]llb.State{stateC, stateB})
|
||||
requireContents(ctx, t, c, sb, mergeB, nil, nil, imageTarget,
|
||||
fstest.CreateFile("a", []byte("A"), 0755),
|
||||
fstest.CreateFile("b", []byte("B"), 0755),
|
||||
fstest.CreateFile("c", []byte("C"), 0755),
|
||||
fstest.CreateDir("bar", 0700),
|
||||
fstest.CreateFile("bar/A", []byte("A"), 0755),
|
||||
fstest.CreateFile("bar/B", []byte("B"), 0754),
|
||||
)
|
||||
|
||||
stateD := llb.Scratch().File(llb.Mkdir("/qaz", 0755))
|
||||
mergeC := llb.Merge([]llb.State{mergeA, mergeB, stateD})
|
||||
requireContents(ctx, t, c, sb, mergeC, nil, nil, imageTarget,
|
||||
fstest.CreateFile("a", []byte("A"), 0755),
|
||||
fstest.CreateFile("b", []byte("B"), 0755),
|
||||
fstest.CreateFile("c", []byte("C"), 0755),
|
||||
fstest.CreateDir("bar", 0700),
|
||||
fstest.CreateFile("bar/A", []byte("A"), 0755),
|
||||
fstest.CreateFile("bar/B", []byte("B"), 0754),
|
||||
fstest.CreateDir("qaz", 0755),
|
||||
)
|
||||
|
||||
runA := runShell(llb.Merge([]llb.State{llb.Image("alpine"), mergeC}),
|
||||
// turn /a file into a dir, mv b and c into it
|
||||
"rm /a",
|
||||
"mkdir /a",
|
||||
"mv /b /c /a/",
|
||||
// remove+recreate /bar to make it opaque on overlay snapshotters
|
||||
"rm -rf /bar",
|
||||
"mkdir -m 0755 /bar",
|
||||
"echo -n D > /bar/D",
|
||||
// turn /qaz dir into a file
|
||||
"rm -rf /qaz",
|
||||
"touch /qaz",
|
||||
)
|
||||
stateE := llb.Scratch().
|
||||
File(llb.Mkfile("/foo", 0755, []byte("E"))).
|
||||
File(llb.Mkdir("/bar", 0755)).
|
||||
File(llb.Mkfile("/bar/A", 0755, []byte("A"))).
|
||||
File(llb.Mkfile("/bar/E", 0755, nil))
|
||||
mergeD := llb.Merge([]llb.State{stateE, runA})
|
||||
requireEqualContents(ctx, t, c, mergeD, llb.Image("alpine").
|
||||
File(llb.Mkdir("a", 0755)).
|
||||
File(llb.Mkfile("a/b", 0755, []byte("B"))).
|
||||
File(llb.Mkfile("a/c", 0755, []byte("C"))).
|
||||
File(llb.Mkdir("bar", 0755)).
|
||||
File(llb.Mkfile("bar/D", 0644, []byte("D"))).
|
||||
File(llb.Mkfile("bar/E", 0755, nil)).
|
||||
File(llb.Mkfile("qaz", 0644, nil)),
|
||||
)
|
||||
// /foo from stateE is not here because it is deleted in stateB, which is part of a submerge of mergeD
|
||||
}
|
||||
|
||||
func testMergeOpCache(t *testing.T, sb integration.Sandbox, mode string) {
|
||||
t.Helper()
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureMergeDiff)
|
||||
requiresLinux(t)
|
||||
|
||||
cdAddress := sb.ContainerdAddress()
|
||||
if cdAddress == "" {
|
||||
t.Skip("test requires containerd worker")
|
||||
}
|
||||
|
||||
client, err := newContainerd(cdAddress)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// push the busybox image to the mutable registry
|
||||
sourceImage := "busybox:latest"
|
||||
def, err := llb.Image(sourceImage).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
busyboxTargetNoTag := registry + "/buildkit/testlazyimage:"
|
||||
busyboxTarget := busyboxTargetNoTag + "latest"
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": busyboxTarget,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imageService := client.ImageService()
|
||||
contentStore := client.ContentStore()
|
||||
|
||||
busyboxImg, err := imageService.Get(ctx, busyboxTarget)
|
||||
require.NoError(t, err)
|
||||
|
||||
busyboxManifest, err := images.Manifest(ctx, contentStore, busyboxImg.Target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, layer := range busyboxManifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// clear all local state out
|
||||
err = imageService.Delete(ctx, busyboxImg.Name, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
for _, layer := range busyboxManifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
// make a new merge that includes the lazy busybox as a base and exports inline cache
|
||||
input1 := llb.Scratch().
|
||||
File(llb.Mkdir("/dir", 0777)).
|
||||
File(llb.Mkfile("/dir/1", 0777, nil))
|
||||
input1Copy := llb.Scratch().File(llb.Copy(input1, "/dir/1", "/foo/1", &llb.CopyInfo{CreateDestPath: true}))
|
||||
|
||||
// put random contents in the file to ensure it's not re-run later
|
||||
input2 := runShell(llb.Image("alpine:latest"),
|
||||
"mkdir /dir",
|
||||
"cat /dev/urandom | head -c 100 | sha256sum > /dir/2")
|
||||
input2Copy := llb.Scratch().File(llb.Copy(input2, "/dir/2", "/bar/2", &llb.CopyInfo{CreateDestPath: true}))
|
||||
|
||||
merge := llb.Merge([]llb.State{llb.Image(busyboxTarget), input1Copy, input2Copy})
|
||||
|
||||
def, err = merge.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
target := registry + "/buildkit/testmerge:latest"
|
||||
cacheTarget := registry + "/buildkit/testmergecache:latest"
|
||||
|
||||
var cacheExports []CacheOptionsEntry
|
||||
var cacheImports []CacheOptionsEntry
|
||||
switch mode {
|
||||
case "inline":
|
||||
cacheExports = []CacheOptionsEntry{{
|
||||
Type: "inline",
|
||||
}}
|
||||
cacheImports = []CacheOptionsEntry{{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{
|
||||
"ref": target,
|
||||
},
|
||||
}}
|
||||
case "min":
|
||||
cacheExports = []CacheOptionsEntry{{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{
|
||||
"ref": cacheTarget,
|
||||
},
|
||||
}}
|
||||
cacheImports = []CacheOptionsEntry{{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{
|
||||
"ref": cacheTarget,
|
||||
},
|
||||
}}
|
||||
case "max":
|
||||
cacheExports = []CacheOptionsEntry{{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{
|
||||
"ref": cacheTarget,
|
||||
"mode": "max",
|
||||
},
|
||||
}}
|
||||
cacheImports = []CacheOptionsEntry{{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{
|
||||
"ref": cacheTarget,
|
||||
},
|
||||
}}
|
||||
default:
|
||||
require.Fail(t, fmt.Sprintf("unknown cache mode: %s", mode))
|
||||
}
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
CacheExports: cacheExports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify that the busybox image stayed lazy
|
||||
for _, layer := range busyboxManifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
// get the random value at /bar/2
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
bar2Contents, err := os.ReadFile(filepath.Join(destDir, "bar", "2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// clear all local state out
|
||||
img, err := imageService.Get(ctx, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, err := images.Manifest(ctx, contentStore, img.Target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
// re-run the same build with cache imports and verify everything stays lazy
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
}},
|
||||
CacheImports: cacheImports,
|
||||
CacheExports: cacheExports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify everything from before stayed lazy
|
||||
img, err = imageService.Get(ctx, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, err = images.Manifest(ctx, contentStore, img.Target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, layer := range manifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v for index %d (%s)", err, i, layer.Digest)
|
||||
}
|
||||
|
||||
// re-run the build with a change only to input1 using the remote cache
|
||||
input1 = llb.Scratch().
|
||||
File(llb.Mkdir("/dir", 0777)).
|
||||
File(llb.Mkfile("/dir/1", 0444, nil))
|
||||
input1Copy = llb.Scratch().File(llb.Copy(input1, "/dir/1", "/foo/1", &llb.CopyInfo{CreateDestPath: true}))
|
||||
|
||||
merge = llb.Merge([]llb.State{llb.Image(busyboxTarget), input1Copy, input2Copy})
|
||||
|
||||
def, err = merge.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
}},
|
||||
CacheExports: cacheExports,
|
||||
CacheImports: cacheImports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify everything from before stayed lazy except the middle layer for input1Copy
|
||||
img, err = imageService.Get(ctx, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, err = images.Manifest(ctx, contentStore, img.Target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, layer := range manifest.Layers {
|
||||
switch i {
|
||||
case 0, 2:
|
||||
// bottom and top layer should stay lazy as they didn't change
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v for index %d", err, i)
|
||||
case 1:
|
||||
// middle layer had to be rebuilt, should exist locally
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
require.Fail(t, fmt.Sprintf("unexpected layer index %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
// check the random value at /bar/2 didn't change
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
CacheImports: cacheImports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newBar2Contents, err := os.ReadFile(filepath.Join(destDir, "bar", "2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equalf(t, bar2Contents, newBar2Contents, "bar/2 contents changed")
|
||||
|
||||
// Now test the case with a layer on top of a merge.
|
||||
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
mergePlusLayer := merge.File(llb.Mkfile("/3", 0444, nil))
|
||||
|
||||
def, err = mergePlusLayer.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
}},
|
||||
CacheExports: cacheExports,
|
||||
CacheImports: cacheImports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check the random value at /bar/2 didn't change
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
CacheImports: cacheImports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newBar2Contents, err = os.ReadFile(filepath.Join(destDir, "bar", "2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equalf(t, bar2Contents, newBar2Contents, "bar/2 contents changed")
|
||||
|
||||
// clear local state, repeat the build, verify everything stays lazy
|
||||
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
||||
}
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
"store": "true",
|
||||
"unsafe-internal-store-allow-incomplete": "true",
|
||||
},
|
||||
}},
|
||||
CacheImports: cacheImports,
|
||||
CacheExports: cacheExports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
img, err = imageService.Get(ctx, target)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, err = images.Manifest(ctx, contentStore, img.Target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, layer := range manifest.Layers {
|
||||
_, err = contentStore.Info(ctx, layer.Digest)
|
||||
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v for index %d", err, i)
|
||||
}
|
||||
}
|
||||
|
||||
func testMergeOpCacheInline(t *testing.T, sb integration.Sandbox) {
|
||||
testMergeOpCache(t, sb, "inline")
|
||||
}
|
||||
|
||||
func testMergeOpCacheMax(t *testing.T, sb integration.Sandbox) {
|
||||
testMergeOpCache(t, sb, "max")
|
||||
}
|
||||
|
||||
func testMergeOpCacheMin(t *testing.T, sb integration.Sandbox) {
|
||||
testMergeOpCache(t, sb, "min")
|
||||
}
|
||||
|
||||
func chainRunShells(base llb.State, cmdss ...[]string) llb.State {
|
||||
for _, cmds := range cmdss {
|
||||
base = runShell(base, cmds...)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func requireContents(ctx context.Context, t *testing.T, c *Client, sb integration.Sandbox, state llb.State, cacheImports, cacheExports []CacheOptionsEntry, imageTarget string, files ...fstest.Applier) {
|
||||
t.Helper()
|
||||
|
||||
def, err := state.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
CacheImports: cacheImports,
|
||||
CacheExports: cacheExports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, fstest.CheckDirectoryEqualWithApplier(destDir, fstest.Apply(files...)))
|
||||
|
||||
if imageTarget != "" {
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": imageTarget,
|
||||
"push": "true",
|
||||
},
|
||||
}},
|
||||
CacheImports: cacheImports,
|
||||
CacheExports: cacheExports,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
resetState(t, c, sb)
|
||||
requireContents(ctx, t, c, sb, llb.Image(imageTarget, llb.ResolveModePreferLocal), cacheImports, nil, "", files...)
|
||||
}
|
||||
}
|
||||
|
||||
func requireEqualContents(ctx context.Context, t *testing.T, c *Client, stateA, stateB llb.State) {
|
||||
t.Helper()
|
||||
|
||||
defA, err := stateA.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
destDirA := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, defA, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDirA,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
defB, err := stateB.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
destDirB := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, defB, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDirB,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, fstest.CheckDirectoryEqual(destDirA, destDirB))
|
||||
}
|
||||
|
||||
func runShell(base llb.State, cmds ...string) llb.State {
|
||||
return runShellExecState(base, cmds...).Root()
|
||||
}
|
||||
|
||||
func runShellExecState(base llb.State, cmds ...string) llb.ExecState {
|
||||
return base.Run(llb.Args([]string{"sh", "-c", strings.Join(cmds, " && ")}))
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,437 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/moby/buildkit/util/testutil/echoserver"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func testBridgeNetworking(t *testing.T, sb integration.Sandbox) {
|
||||
if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
if sb.Rootless() { // bridge is not used by default, even with detach-netns
|
||||
t.SkipNow()
|
||||
}
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
s, err := echoserver.NewTestServer("foo")
|
||||
require.NoError(t, err)
|
||||
addrParts := strings.Split(s.Addr().String(), ":")
|
||||
|
||||
def, err := llb.Image("busybox").Run(llb.Shlexf("sh -c 'nc 127.0.0.1 %s | grep foo'", addrParts[len(addrParts)-1])).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func testBridgeNetworkingDNSNoRootless(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureCNINetwork)
|
||||
if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
name := identity.NewID()
|
||||
server, err := llb.Image("busybox").
|
||||
Run(
|
||||
llb.Shlexf(`sh -c 'test "$(nc -l -p 1234)" = "foo"'`),
|
||||
llb.Hostname(name),
|
||||
).
|
||||
Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := llb.Image("busybox").
|
||||
Run(
|
||||
llb.Shlexf("sh -c 'until echo foo | nc " + name + " 1234 -w0; do sleep 0.1; done'"),
|
||||
).
|
||||
Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
eg, ctx := errgroup.WithContext(context.Background())
|
||||
eg.Go(func() error {
|
||||
_, err := c.Solve(ctx, server, SolveOpt{}, nil)
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
_, err := c.Solve(ctx, client, SolveOpt{}, nil)
|
||||
return err
|
||||
})
|
||||
err = eg.Wait()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
/*
|
||||
testExtraHosts verifies that custom host entries added via llb.AddExtraHost() are resolvable
|
||||
during a RUN step. It adds "myhost" pointing to 1.2.3.4 and checks /etc/hosts for the entry.
|
||||
|
||||
Skipped on Windows because BuildKit for Windows does not support llb.AddExtraHost()
|
||||
at the moment due to fundamental differences in how Linux and Windows containers handle hosts file injections
|
||||
*/
|
||||
func testExtraHosts(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows", "extra hosts not supported on BuildKit for Windows at the moment")
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh -c 'cat /etc/hosts | grep myhost | grep 1.2.3.4'`), llb.AddExtraHost("myhost", net.ParseIP("1.2.3.4")))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testHostNetworking(t *testing.T, sb integration.Sandbox) {
|
||||
if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
netMode := sb.Value("netmode")
|
||||
var allowedEntitlements []string
|
||||
if netMode == hostNetwork {
|
||||
allowedEntitlements = []string{entitlements.EntitlementNetworkHost.String()}
|
||||
}
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
s, err := echoserver.NewTestServer("foo")
|
||||
require.NoError(t, err)
|
||||
addrParts := strings.Split(s.Addr().String(), ":")
|
||||
|
||||
def, err := llb.Image("busybox").Run(llb.Shlexf("sh -c 'nc 127.0.0.1 %s | grep foo'", addrParts[len(addrParts)-1]), llb.Network(llb.NetModeHost)).Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
AllowedEntitlements: allowedEntitlements,
|
||||
}, nil)
|
||||
if netMode == hostNetwork {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testHostnameLookup(t *testing.T, sb integration.Sandbox) {
|
||||
if sb.Rootless() { // bridge is not used by default, even with detach-netns
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
`sh -c "ping -c 1 $(hostname)"`,
|
||||
"cmd /C ping -n 1 %COMPUTERNAME%",
|
||||
)
|
||||
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// moby/buildkit#1301
|
||||
func testHostnameSpecifying(t *testing.T, sb integration.Sandbox) {
|
||||
if sb.Rootless() { // bridge is not used by default, even with detach-netns
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
type testVars struct {
|
||||
imgName string
|
||||
cmdStr1 string
|
||||
cmdStr2 string
|
||||
}
|
||||
|
||||
v := integration.UnixOrWindows(
|
||||
testVars{
|
||||
imgName: "busybox:latest",
|
||||
cmdStr1: "sh -c 'echo $HOSTNAME | grep %s'",
|
||||
cmdStr2: "sh -c 'echo $(hostname) | grep %s'",
|
||||
},
|
||||
testVars{
|
||||
imgName: "nanoserver:latest",
|
||||
// NOTE: Windows capitalizes the hostname hence
|
||||
// case insensitive findstr (findstr /I)
|
||||
// testtest --> TESTTEST
|
||||
cmdStr1: "cmd /C echo %%COMPUTERNAME%% | findstr /I %s",
|
||||
cmdStr2: "cmd /C echo %%COMPUTERNAME%% | findstr /I %s",
|
||||
},
|
||||
)
|
||||
|
||||
hostname := "testtest"
|
||||
st := llb.Image(v.imgName).With(llb.Hostname(hostname)).
|
||||
Run(llb.Shlexf(v.cmdStr1, hostname)).
|
||||
Run(llb.Shlexf(v.cmdStr2, hostname))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
FrontendAttrs: map[string]string{"hostname": hostname},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testNetworkMode(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows")
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh -c 'wget https://example.com 2>&1 | grep "wget: bad address"'`), llb.Network(llb.NetModeNone))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
st2 := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`ifconfig`), llb.Network(llb.NetModeHost))
|
||||
|
||||
def, err = st2.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
// Currently disabled globally by default
|
||||
// AllowedEntitlements: []entitlements.Entitlement{entitlements.EntitlementNetworkHost},
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "network.host is not allowed")
|
||||
}
|
||||
|
||||
func testProxyEnv(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")
|
||||
scratch := func() llb.State {
|
||||
return integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
||||
}
|
||||
base := llb.Image(imgName).Dir("/out")
|
||||
cmd := integration.UnixOrWindows(
|
||||
`sh -c "echo -n $HTTP_PROXY-$HTTPS_PROXY-$NO_PROXY-$no_proxy-$ALL_PROXY-$all_proxy > env"`,
|
||||
`cmd /C echo %HTTP_PROXY%-%HTTPS_PROXY%-%NO_PROXY%-%no_proxy%-%ALL_PROXY%-%all_proxy% > env`,
|
||||
)
|
||||
|
||||
st := base.Run(llb.Shlex(cmd), llb.WithProxy(llb.ProxyEnv{
|
||||
HTTPProxy: "httpvalue",
|
||||
HTTPSProxy: "httpsvalue",
|
||||
NoProxy: "noproxyvalue",
|
||||
AllProxy: "allproxyvalue",
|
||||
}))
|
||||
|
||||
out := st.AddMount("/out", scratch())
|
||||
|
||||
def, err := out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "env"))
|
||||
require.NoError(t, err)
|
||||
newLine := integration.UnixOrWindows("", " \r\n")
|
||||
require.Equal(t, "httpvalue-httpsvalue-noproxyvalue-noproxyvalue-allproxyvalue-allproxyvalue"+newLine, string(dt))
|
||||
|
||||
// repeat to make sure proxy doesn't change cache
|
||||
st = base.Run(llb.Shlex(cmd), llb.WithProxy(llb.ProxyEnv{
|
||||
HTTPSProxy: "httpsvalue2",
|
||||
NoProxy: "noproxyvalue2",
|
||||
}))
|
||||
out = st.AddMount("/out", scratch())
|
||||
|
||||
def, err = out.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir = t.TempDir()
|
||||
|
||||
_, 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, "env"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "httpvalue-httpsvalue-noproxyvalue-noproxyvalue-allproxyvalue-allproxyvalue"+newLine, string(dt))
|
||||
}
|
||||
|
||||
func testResolveAndHosts(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
busybox := llb.Image("busybox:latest")
|
||||
st := llb.Scratch()
|
||||
|
||||
run := func(cmd string) {
|
||||
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
||||
}
|
||||
|
||||
run(`sh -c "cp /etc/resolv.conf ."`)
|
||||
run(`sh -c "cp /etc/hosts ."`)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, 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, "resolv.conf"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(dt), "nameserver")
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "hosts"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(dt), "127.0.0.1 localhost")
|
||||
}
|
||||
|
||||
type netModeHost struct{}
|
||||
|
||||
func (*netModeHost) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + "\n\ninsecure-entitlements = [\"network.host\"]\n", nil
|
||||
}
|
||||
|
||||
type netModeProxyBridge struct{}
|
||||
|
||||
func (*netModeProxyBridge) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + `
|
||||
|
||||
insecure-entitlements = ["network.host"]
|
||||
|
||||
[worker.oci]
|
||||
networkMode = "bridge"
|
||||
|
||||
[worker.containerd]
|
||||
networkMode = "bridge"
|
||||
`, nil
|
||||
}
|
||||
|
||||
type netModeProxyDefault struct{}
|
||||
|
||||
func (*netModeProxyDefault) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + `
|
||||
|
||||
insecure-entitlements = ["network.host"]
|
||||
`, nil
|
||||
}
|
||||
|
||||
type netModeProxyDefaultNoCNI struct{}
|
||||
|
||||
func (*netModeProxyDefaultNoCNI) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + `
|
||||
|
||||
insecure-entitlements = ["network.host"]
|
||||
|
||||
[worker.oci]
|
||||
cniConfigPath = "/tmp/buildkit-missing-cni.json"
|
||||
|
||||
[worker.containerd]
|
||||
cniConfigPath = "/tmp/buildkit-missing-cni.json"
|
||||
`, nil
|
||||
}
|
||||
|
||||
type netModeProxyHost struct{}
|
||||
|
||||
func (*netModeProxyHost) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + `
|
||||
|
||||
insecure-entitlements = ["network.host"]
|
||||
|
||||
[worker.oci]
|
||||
networkMode = "host"
|
||||
|
||||
[worker.containerd]
|
||||
networkMode = "host"
|
||||
`, nil
|
||||
}
|
||||
|
||||
type netModeDefault struct{}
|
||||
|
||||
func (*netModeDefault) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
type netModeBridgeDNS struct{}
|
||||
|
||||
func (*netModeBridgeDNS) UpdateConfigFile(in string) (string, func() error) {
|
||||
return in + `
|
||||
# configure bridge networking
|
||||
[worker.oci]
|
||||
networkMode = "cni"
|
||||
cniConfigPath = "/etc/buildkit/dns-cni.conflist"
|
||||
|
||||
[worker.containerd]
|
||||
networkMode = "cni"
|
||||
cniConfigPath = "/etc/buildkit/dns-cni.conflist"
|
||||
|
||||
[dns]
|
||||
nameservers = ["10.11.0.1"]
|
||||
`, nil
|
||||
}
|
||||
|
||||
var (
|
||||
hostNetwork integration.ConfigUpdater = &netModeHost{}
|
||||
defaultNetwork integration.ConfigUpdater = &netModeDefault{}
|
||||
proxyDefaultNetwork integration.ConfigUpdater = &netModeProxyDefault{}
|
||||
proxyDefaultNetworkNoCNI integration.ConfigUpdater = &netModeProxyDefaultNoCNI{}
|
||||
proxyBridgeNetwork integration.ConfigUpdater = &netModeProxyBridge{}
|
||||
proxyHostNetwork integration.ConfigUpdater = &netModeProxyHost{}
|
||||
bridgeDNSNetwork integration.ConfigUpdater = &netModeBridgeDNS{}
|
||||
)
|
||||
@@ -0,0 +1,606 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/plugins/content/local"
|
||||
"github.com/containerd/platforms"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
"github.com/moby/buildkit/util/contentutil"
|
||||
"github.com/moby/buildkit/util/iohelper"
|
||||
"github.com/moby/buildkit/util/purl"
|
||||
"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"
|
||||
packageurl "github.com/package-url/packageurl-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testImageBlobSource(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
st := llb.Image("alpine")
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
name := registry + "/foo/blobtest:img"
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: "image",
|
||||
Attrs: map[string]string{
|
||||
"name": name,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc, provider, err := contentutil.ProviderFromRef(name)
|
||||
require.NoError(t, err)
|
||||
|
||||
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(imgs.Images))
|
||||
mfst := imgs.Images[0].Manifest
|
||||
require.GreaterOrEqual(t, len(mfst.Layers), 1)
|
||||
|
||||
l := mfst.Layers[0]
|
||||
|
||||
blob := llb.ImageBlob(registry+"/foo/blobtest@"+l.Digest.String(), llb.Filename("layer.tar.gz"), llb.Chown(123, 456))
|
||||
st = llb.Image("alpine").Run(llb.Shlex(`sh -c 'sha256sum /layers/layer.tar.gz | cut -d" " -f0 > /out/checksum && stat -c "%u-%g-%s" /layers/layer.tar.gz > /out/stat'`), llb.AddMount("/layers", blob, llb.Readonly)).AddMount("/out", llb.Scratch())
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"attest:provenance": "",
|
||||
},
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "stat"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "123-456-"+strconv.FormatInt(l.Size, 10), strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "checksum"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, l.Digest.Hex(), strings.TrimSpace(string(dt)))
|
||||
|
||||
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var stmt struct {
|
||||
intoto.StatementHeader
|
||||
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(provDt, &stmt))
|
||||
|
||||
expectedName, err := purl.RefToPURL(packageurl.TypeDocker, registry+"/foo/blobtest@"+l.Digest.String(), nil)
|
||||
require.NoError(t, err)
|
||||
purlObj, err := packageurl.FromString(expectedName)
|
||||
require.NoError(t, err)
|
||||
purlObj.Qualifiers = append(purlObj.Qualifiers, packageurl.Qualifier{Key: "ref_type", Value: "blob"})
|
||||
expectedName = purlObj.ToString()
|
||||
|
||||
found := false
|
||||
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
|
||||
if m.URI == expectedName {
|
||||
found = true
|
||||
require.Equal(t, l.Digest.Hex(), m.Digest["sha256"])
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected to find %q in %+v", expectedName, stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
}
|
||||
|
||||
func testNoTarOCIIndexMediaType(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")
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
`sh -c "echo -n hello > hello"`,
|
||||
`cmd /C "echo hello> hello"`,
|
||||
)
|
||||
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir, err := os.MkdirTemp("", "buildkit")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(destDir)
|
||||
|
||||
outDir := filepath.Join(destDir, "out.d")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Attrs: map[string]string{
|
||||
"tar": "false",
|
||||
},
|
||||
OutputDir: outDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(outDir, "index.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(dt, &index)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "application/vnd.oci.image.index.v1+json", index.MediaType)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testOCIIndexMediatype(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")
|
||||
cmdStr := integration.UnixOrWindows(
|
||||
`sh -c "echo -n hello > hello"`,
|
||||
`cmd /C "echo hello> hello"`,
|
||||
)
|
||||
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir, err := os.MkdirTemp("", "buildkit")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(destDir)
|
||||
|
||||
out := filepath.Join(destDir, "out.tar")
|
||||
outW, err := os.Create(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Output: fixedWriteCloser(outW),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexDt, ok := m[ocispecs.ImageIndexFile]
|
||||
require.True(t, ok)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(indexDt.Data, &index)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "application/vnd.oci.image.index.v1+json", index.MediaType)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testOCILayoutBlobSource(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("alpine")
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
ociDir := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Attrs: map[string]string{
|
||||
"tar": "false",
|
||||
},
|
||||
OutputDir: ociDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexDt, err := os.ReadFile(filepath.Join(ociDir, ocispecs.ImageIndexFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
var index ocispecs.Index
|
||||
err = json.Unmarshal(indexDt, &index)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(index.Manifests))
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
mfstDt, err := os.ReadFile(filepath.Join(ociDir, "blobs/sha256", index.Manifests[0].Digest.Hex()))
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(mfstDt, &mfst)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(mfst.Layers), 1)
|
||||
layer := mfst.Layers[0]
|
||||
|
||||
store, err := local.NewStore(ociDir)
|
||||
require.NoError(t, err)
|
||||
csID := "my-blob-content-store"
|
||||
|
||||
blob := llb.OCILayoutBlob("not/real@"+layer.Digest.String(), llb.ImageBlobOCIStore("", csID), llb.Filename("layer.tar.gz"), llb.Chown(123, 456))
|
||||
st = llb.Image("alpine").Run(llb.Shlex(`sh -c 'sha256sum /layers/layer.tar.gz | cut -d" " -f0 > /out/checksum && stat -c "%u-%g-%s" /layers/layer.tar.gz > /out/stat'`), llb.AddMount("/layers", blob, llb.Readonly)).AddMount("/out", llb.Scratch())
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
FrontendAttrs: map[string]string{
|
||||
"attest:provenance": "",
|
||||
},
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "stat"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "123-456-"+strconv.FormatInt(layer.Size, 10), strings.TrimSpace(string(dt)))
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "checksum"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, layer.Digest.Hex(), strings.TrimSpace(string(dt)))
|
||||
|
||||
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var stmt struct {
|
||||
intoto.StatementHeader
|
||||
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(provDt, &stmt))
|
||||
|
||||
expectedName, err := purl.RefToPURL(packageurl.TypeOCI, "not/real@"+layer.Digest.String(), nil)
|
||||
require.NoError(t, err)
|
||||
purlObj, err := packageurl.FromString(expectedName)
|
||||
require.NoError(t, err)
|
||||
purlObj.Qualifiers = append(purlObj.Qualifiers, packageurl.Qualifier{Key: "ref_type", Value: "blob"})
|
||||
expectedName = purlObj.ToString()
|
||||
|
||||
found := false
|
||||
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
|
||||
if m.URI == expectedName {
|
||||
found = true
|
||||
require.Equal(t, layer.Digest.Hex(), m.Digest["sha256"])
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected to find %q in %+v", expectedName, stmt.Predicate.BuildDefinition.ResolvedDependencies)
|
||||
}
|
||||
|
||||
func testOCILayoutPlatformSource(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
|
||||
requiresLinux(t)
|
||||
c, err := New(context.TODO(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// create a tempdir where we will store the OCI layout
|
||||
dir := t.TempDir()
|
||||
|
||||
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("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
|
||||
}
|
||||
attrs := map[string]string{}
|
||||
outW := bytes.NewBuffer(nil)
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Attrs: attrs,
|
||||
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
|
||||
},
|
||||
},
|
||||
}, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// extract the tar stream to the directory as OCI layout
|
||||
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
for filename, tarItem := range m {
|
||||
fullFilename := path.Join(dir, filename)
|
||||
err = os.MkdirAll(path.Dir(fullFilename), 0755)
|
||||
require.NoError(t, err)
|
||||
if tarItem.Header.FileInfo().IsDir() {
|
||||
err = os.MkdirAll(fullFilename, 0755)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
err = os.WriteFile(fullFilename, tarItem.Data, 0644)
|
||||
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
|
||||
|
||||
store, err := local.NewStore(dir)
|
||||
require.NoError(t, err)
|
||||
csID := "my-content-store"
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
frontendOCILayout := 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.OCILayout(fmt.Sprintf("not/real@%s", digest), llb.OCIStore("", csID))
|
||||
|
||||
def, err := st.Marshal(ctx, llb.Platform(platforms.MustParse(platform)))
|
||||
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
|
||||
}
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, "", frontendOCILayout, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, platform := range platformsToTest {
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_"), "platform"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(platform), dt)
|
||||
}
|
||||
}
|
||||
|
||||
func testOCILayoutSource(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
|
||||
c, err := New(context.TODO(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
// create a tempdir where we will store the OCI layout
|
||||
dir := t.TempDir()
|
||||
|
||||
// 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)
|
||||
|
||||
outW := bytes.NewBuffer(nil)
|
||||
attrs := map[string]string{}
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterOCI,
|
||||
Attrs: attrs,
|
||||
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// extract the tar stream to the directory as OCI layout
|
||||
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
for filename, content := range m {
|
||||
fullFilename := path.Join(dir, filename)
|
||||
err = os.MkdirAll(path.Dir(fullFilename), 0755)
|
||||
require.NoError(t, err)
|
||||
if content.Header.FileInfo().IsDir() {
|
||||
err = os.MkdirAll(fullFilename, 0755)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
err = os.WriteFile(fullFilename, content.Data, 0644)
|
||||
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
|
||||
|
||||
store, err := local.NewStore(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// reference the OCI Layout in a build
|
||||
// note that the key does not need to be the directory name, just something
|
||||
// unique. since we are doing just one build with one remote here, we can
|
||||
// give it any ID
|
||||
csID := "my-content-store"
|
||||
st = llb.OCILayout(fmt.Sprintf("not/real@%s", digest), llb.OCIStore("", csID))
|
||||
|
||||
def, err = st.Marshal(context.TODO())
|
||||
require.NoError(t, err)
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(context.TODO(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
OCIStores: map[string]content.Store{
|
||||
csID: store,
|
||||
},
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(destDir, "foo"))
|
||||
require.NoError(t, err)
|
||||
newLine := integration.UnixOrWindows("", " \r\n")
|
||||
require.Equal(t, []byte("first"+newLine), dt)
|
||||
|
||||
dt, err = os.ReadFile(filepath.Join(destDir, "bar"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("second"+newLine), dt)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/session"
|
||||
sessionsecrets "github.com/moby/buildkit/session/secrets"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func testSessionHealthMonitorFailsBlockedSecretResponse(t *testing.T, sb integration.Sandbox) {
|
||||
integration.SkipOnPlatform(t, "windows", "Windows secret mounts are covered separately")
|
||||
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
st := llb.Image("busybox:latest").
|
||||
Run(llb.Shlex(`sh -c '[ "$(cat /run/secrets/mysecret)" = "foo-secret" ]'`), llb.AddSecret("/run/secrets/mysecret"))
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
provider := &gatedSecretProvider{
|
||||
t: t,
|
||||
id: "/run/secrets/mysecret",
|
||||
data: []byte("foo-secret"),
|
||||
started: make(chan struct{}),
|
||||
allowResponse: make(chan struct{}),
|
||||
}
|
||||
|
||||
baseDialer := c.Dialer()
|
||||
proxyCh := make(chan *gatedSessionTunnelProxy, 1)
|
||||
c.sessionDialer = func(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
|
||||
meta["X-Buildkit-Session-Health-Custom-Timeout"] = []string{"2000"}
|
||||
|
||||
conn, err := baseDialer(ctx, proto, meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxy, clientConn := newGatedSessionTunnelProxy(conn)
|
||||
proxyCh <- proxy
|
||||
return clientConn, nil
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := c.Solve(sb.Context(), def, SolveOpt{
|
||||
Session: []session.Attachable{provider},
|
||||
}, nil)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
var stalledConn *gatedSessionTunnelProxy
|
||||
select {
|
||||
case stalledConn = <-proxyCh:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timed out waiting for session connection")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-provider.started:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timed out waiting for secret request")
|
||||
}
|
||||
|
||||
stalledConn.blockClientToDaemon.Store(true)
|
||||
close(provider.allowResponse)
|
||||
|
||||
select {
|
||||
case <-stalledConn.blockedClientToDaemon:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timed out waiting for blocked session traffic")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.Error(t, err)
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timed out waiting for solve to fail after session healthcheck timeout")
|
||||
}
|
||||
}
|
||||
|
||||
type gatedSecretProvider struct {
|
||||
sessionsecrets.UnimplementedSecretsServer
|
||||
|
||||
t *testing.T
|
||||
id string
|
||||
data []byte
|
||||
started chan struct{}
|
||||
allowResponse chan struct{}
|
||||
}
|
||||
|
||||
func (p *gatedSecretProvider) Register(server *grpc.Server) {
|
||||
sessionsecrets.RegisterSecretsServer(server, p)
|
||||
}
|
||||
|
||||
func (p *gatedSecretProvider) GetSecret(ctx context.Context, req *sessionsecrets.GetSecretRequest) (*sessionsecrets.GetSecretResponse, error) {
|
||||
require.Equal(p.t, p.id, req.ID)
|
||||
select {
|
||||
case <-p.started:
|
||||
default:
|
||||
close(p.started)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, context.Cause(ctx)
|
||||
case <-p.allowResponse:
|
||||
return &sessionsecrets.GetSecretResponse{Data: p.data}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type gatedSessionTunnelProxy struct {
|
||||
clientConn net.Conn
|
||||
proxyConn net.Conn
|
||||
daemonConn net.Conn
|
||||
|
||||
blockClientToDaemon atomic.Bool
|
||||
blockedClientToDaemon chan struct{}
|
||||
releaseClientToDaemon chan struct{}
|
||||
closed chan struct{}
|
||||
blockedOnce sync.Once
|
||||
releaseOnce sync.Once
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (p *gatedSessionTunnelProxy) Close() error {
|
||||
p.closeOnce.Do(func() {
|
||||
close(p.closed)
|
||||
_ = p.proxyConn.Close()
|
||||
_ = p.clientConn.Close()
|
||||
_ = p.daemonConn.Close()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *gatedSessionTunnelProxy) Release() {
|
||||
p.releaseOnce.Do(func() {
|
||||
close(p.releaseClientToDaemon)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *gatedSessionTunnelProxy) copyClientToDaemon() {
|
||||
defer p.Close()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := p.proxyConn.Read(buf)
|
||||
if n > 0 {
|
||||
data := append([]byte(nil), buf[:n]...)
|
||||
if p.blockClientToDaemon.Load() {
|
||||
p.blockedOnce.Do(func() {
|
||||
close(p.blockedClientToDaemon)
|
||||
})
|
||||
select {
|
||||
case <-p.releaseClientToDaemon:
|
||||
case <-p.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, writeErr := p.daemonConn.Write(data); writeErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *gatedSessionTunnelProxy) copyDaemonToClient() {
|
||||
defer p.Close()
|
||||
_, _ = io.Copy(p.proxyConn, p.daemonConn)
|
||||
}
|
||||
|
||||
func newGatedSessionTunnelProxy(daemonConn net.Conn) (*gatedSessionTunnelProxy, net.Conn) {
|
||||
clientConn, proxyConn := net.Pipe()
|
||||
p := &gatedSessionTunnelProxy{
|
||||
clientConn: clientConn,
|
||||
proxyConn: proxyConn,
|
||||
daemonConn: daemonConn,
|
||||
blockedClientToDaemon: make(chan struct{}),
|
||||
releaseClientToDaemon: make(chan struct{}),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go p.copyClientToDaemon()
|
||||
go p.copyDaemonToClient()
|
||||
return p, clientConn
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/solver/errdefs"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testSourceMap(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
sm1 := llb.NewSourceMap(nil, "foo", "", []byte("data1"))
|
||||
sm2 := llb.NewSourceMap(nil, "bar", "", []byte("data2"))
|
||||
|
||||
st := llb.Scratch().Run(
|
||||
llb.Shlex("not-exist"),
|
||||
sm1.Location([]*pb.Range{{Start: &pb.Position{Line: 7}}}),
|
||||
sm2.Location([]*pb.Range{{Start: &pb.Position{Line: 8}}}),
|
||||
sm1.Location([]*pb.Range{{Start: &pb.Position{Line: 9}}}),
|
||||
)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
srcs := errdefs.Sources(err)
|
||||
require.Equal(t, 3, len(srcs))
|
||||
|
||||
// Source errors are wrapped in the order provided as llb.ConstraintOpts, so
|
||||
// when they are unwrapped, the first unwrapped error is the last location
|
||||
// provided.
|
||||
require.Equal(t, "foo", srcs[0].Info.Filename)
|
||||
require.Equal(t, []byte("data1"), srcs[0].Info.Data)
|
||||
require.Nil(t, srcs[0].Info.Definition)
|
||||
|
||||
require.Equal(t, 1, len(srcs[0].Ranges))
|
||||
require.Equal(t, int32(9), srcs[0].Ranges[0].Start.Line)
|
||||
require.Equal(t, int32(0), srcs[0].Ranges[0].Start.Character)
|
||||
|
||||
require.Equal(t, "bar", srcs[1].Info.Filename)
|
||||
require.Equal(t, []byte("data2"), srcs[1].Info.Data)
|
||||
require.Nil(t, srcs[1].Info.Definition)
|
||||
|
||||
require.Equal(t, 1, len(srcs[1].Ranges))
|
||||
require.Equal(t, int32(8), srcs[1].Ranges[0].Start.Line)
|
||||
require.Equal(t, int32(0), srcs[1].Ranges[0].Start.Character)
|
||||
|
||||
require.Equal(t, "foo", srcs[2].Info.Filename)
|
||||
require.Equal(t, []byte("data1"), srcs[2].Info.Data)
|
||||
require.Nil(t, srcs[2].Info.Definition)
|
||||
|
||||
require.Equal(t, 1, len(srcs[2].Ranges))
|
||||
require.Equal(t, int32(7), srcs[2].Ranges[0].Start.Line)
|
||||
require.Equal(t, int32(0), srcs[2].Ranges[0].Start.Character)
|
||||
}
|
||||
|
||||
func testSourceMapFromRef(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
srcState := llb.Scratch().File(
|
||||
llb.Mkfile("foo", 0600, []byte("data")))
|
||||
sm := llb.NewSourceMap(&srcState, "bar", "mylang", []byte("bardata"))
|
||||
|
||||
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
st := llb.Scratch().File(
|
||||
llb.Mkdir("foo/bar", 0600), // fails because /foo doesn't exist
|
||||
sm.Location([]*pb.Range{{Start: &pb.Position{Line: 3, Character: 1}}}),
|
||||
)
|
||||
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref, err := res.SingleRef()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st2, err := ref.ToState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st = llb.Scratch().File(
|
||||
llb.Copy(st2, "foo", "foo2"),
|
||||
)
|
||||
|
||||
def, err = st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}
|
||||
|
||||
_, err = c.Build(sb.Context(), SolveOpt{}, "", frontend, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
srcs := errdefs.Sources(err)
|
||||
require.Equal(t, 1, len(srcs))
|
||||
|
||||
require.Equal(t, "bar", srcs[0].Info.Filename)
|
||||
require.Equal(t, "mylang", srcs[0].Info.Language)
|
||||
require.Equal(t, []byte("bardata"), srcs[0].Info.Data)
|
||||
require.NotNil(t, srcs[0].Info.Definition)
|
||||
|
||||
require.Equal(t, 1, len(srcs[0].Ranges))
|
||||
require.Equal(t, int32(3), srcs[0].Ranges[0].Start.Line)
|
||||
require.Equal(t, int32(1), srcs[0].Ranges[0].Start.Character)
|
||||
}
|
||||
+264
-14937
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,328 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ctd "github.com/containerd/containerd/v2/client"
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/content/proxy"
|
||||
"github.com/containerd/containerd/v2/core/snapshots"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
controlapi "github.com/moby/buildkit/api/services/control"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/session/filesync"
|
||||
"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/require"
|
||||
fsutiltypes "github.com/tonistiigi/fsutil/types"
|
||||
)
|
||||
|
||||
func checkAllReleasable(t *testing.T, c *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.Greater(t, 20, retries)
|
||||
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, 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 == "" {
|
||||
if checkContent {
|
||||
if err := workers.HasFeatureCompat(t, sb, workers.FeatureContentCheck); err == nil {
|
||||
store := proxy.NewContentStore(c.ContentClient())
|
||||
count := 0
|
||||
err := store.Walk(sb.Context(), func(info content.Info) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
}
|
||||
}
|
||||
t.Logf("checkAllReleasable: skipping check for exported tars in non-containerd test")
|
||||
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()
|
||||
|
||||
for _, ns := range []string{"buildkit", "buildkit_history"} {
|
||||
ctx := namespaces.WithNamespace(sb.Context(), ns)
|
||||
snapshotterName := sb.Snapshotter()
|
||||
snapshotService := client.SnapshotService(snapshotterName)
|
||||
|
||||
checkLeases := func() {
|
||||
leases, err := client.LeasesService().List(ctx)
|
||||
require.NoError(t, err)
|
||||
count := 0
|
||||
for _, l := range leases {
|
||||
_, isTemp := l.Labels["buildkit/lease.temporary"]
|
||||
_, isExpire := l.Labels["containerd.io/gc.expire"]
|
||||
if isTemp && isExpire {
|
||||
continue
|
||||
}
|
||||
resources, err := client.LeasesService().ListResources(ctx, l)
|
||||
require.NoError(t, err)
|
||||
if len(resources) == 0 {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
t.Logf("lease: %v", l)
|
||||
for _, r := range resources {
|
||||
t.Logf("lease resource: %v", r)
|
||||
}
|
||||
}
|
||||
require.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
if checkContent {
|
||||
images, err := client.ImageService().List(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, img := range images {
|
||||
err := client.ImageService().Delete(ctx, img.Name)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
checkLeases()
|
||||
return
|
||||
}
|
||||
|
||||
retries = 0
|
||||
for {
|
||||
count := 0
|
||||
var infos []content.Info
|
||||
err = client.ContentStore().Walk(ctx, func(info content.Info) error {
|
||||
count++
|
||||
infos = append(infos, info)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
if retries >= 50 {
|
||||
for _, info := range infos {
|
||||
t.Logf("content: %v %v %+v", info.Digest, info.Size, info.Labels)
|
||||
ra, err := client.ContentStore().ReaderAt(ctx, ocispecs.Descriptor{
|
||||
Digest: info.Digest,
|
||||
Size: info.Size,
|
||||
})
|
||||
if err == nil {
|
||||
dt := make([]byte, 1024)
|
||||
n, err := ra.ReadAt(dt, 0)
|
||||
t.Logf("data: %+v %q", err, string(dt[:n]))
|
||||
}
|
||||
}
|
||||
require.FailNowf(t, "content still exists", "%+v", infos)
|
||||
}
|
||||
retries++
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
checkLeases()
|
||||
}
|
||||
}
|
||||
|
||||
func ensureFile(t *testing.T, path string) {
|
||||
st, err := os.Stat(path)
|
||||
require.NoError(t, err, "expected file at %s", path)
|
||||
require.True(t, st.Mode().IsRegular())
|
||||
}
|
||||
|
||||
func ensureFileContents(t *testing.T, path, expectedContents string) {
|
||||
contents, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualContents := string(contents)
|
||||
require.Equal(t, expectedContents, actualContents,
|
||||
"file contents mismatch for %s\nexpected: %q\nactual: %q", path, expectedContents, actualContents)
|
||||
}
|
||||
|
||||
// ensurePruneAll tries to ensure Prune completes with retries.
|
||||
// Current cache implementation defers release-related logic using goroutine so
|
||||
// there can be situation where a build has finished but the following prune doesn't
|
||||
// cleanup cache because some records still haven't been released.
|
||||
// This function tries to ensure prune by retrying it.
|
||||
func ensurePruneAll(t *testing.T, c *Client, sb integration.Sandbox) {
|
||||
for i := range 2 {
|
||||
require.NoError(t, c.Prune(sb.Context(), nil, PruneAll))
|
||||
for range 20 {
|
||||
du, err := c.DiskUsage(sb.Context())
|
||||
require.NoError(t, err)
|
||||
if len(du) == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
t.Logf("retrying prune(%d)", i)
|
||||
}
|
||||
t.Fatalf("failed to ensure prune")
|
||||
}
|
||||
|
||||
func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc {
|
||||
return func(map[string]string) (io.WriteCloser, error) {
|
||||
return wc, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newContainerd(cdAddress string) (*ctd.Client, error) {
|
||||
return ctd.New(cdAddress, ctd.WithTimeout(60*time.Second))
|
||||
}
|
||||
|
||||
func parseFSMetadata(t *testing.T, dt []byte) []fsutiltypes.Stat {
|
||||
var m []fsutiltypes.Stat
|
||||
for len(dt) > 0 {
|
||||
var s fsutiltypes.Stat
|
||||
n := binary.LittleEndian.Uint32(dt[:4])
|
||||
dt = dt[4:]
|
||||
err := s.Unmarshal(dt[:n])
|
||||
require.NoError(t, err)
|
||||
m = append(m, *s.CloneVT())
|
||||
dt = dt[n:]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func readFileInImage(ctx context.Context, t *testing.T, c *Client, ref, path string) ([]byte, error) {
|
||||
def, err := llb.Image(ref).Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destDir := t.TempDir()
|
||||
|
||||
_, err = c.Solve(ctx, def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: destDir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(filepath.Join(destDir, filepath.Clean(path)))
|
||||
}
|
||||
|
||||
type imageTimestamps struct {
|
||||
FromImage []string // from img.Created and img.[]History.Created
|
||||
FromAnnotation string // from index.Manifests[0].Annotations["org.opencontainers.image.created"]
|
||||
}
|
||||
|
||||
func readImageTimestamps(dt []byte) (*imageTimestamps, error) {
|
||||
m, err := testutil.ReadTarToMap(dt, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := m["oci-layout"]; !ok {
|
||||
return nil, errors.Errorf("no oci-layout")
|
||||
}
|
||||
|
||||
var index ocispecs.Index
|
||||
if err := json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(index.Manifests) != 1 {
|
||||
return nil, errors.Errorf("invalid manifest count %d", len(index.Manifests))
|
||||
}
|
||||
|
||||
var res imageTimestamps
|
||||
res.FromAnnotation = index.Manifests[0].Annotations[ocispecs.AnnotationCreated]
|
||||
|
||||
var mfst ocispecs.Manifest
|
||||
if err := json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// don't unmarshal to image type so we get the original string value
|
||||
type history struct {
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
img := struct {
|
||||
History []history `json:"history"`
|
||||
Created string `json:"created"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Config.Digest.Hex()].Data, &img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.FromImage = []string{
|
||||
img.Created,
|
||||
}
|
||||
for _, h := range img.History {
|
||||
res.FromImage = append(res.FromImage, h.Created)
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func requiresLinux(t *testing.T) {
|
||||
integration.SkipOnPlatform(t, "!linux")
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testWarnings(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
product := "buildkit_test"
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Scratch().File(llb.Mkfile("/dummy", 0o600, []byte("foo")))
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
dgst, _, _, _, err := st.Output().Vertex(ctx, def.Constraints).Marshal(ctx, def.Constraints)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
require.NoError(t, c.Warn(ctx, dgst, "this is warning", client.WarnOpts{
|
||||
Level: 3,
|
||||
SourceInfo: &pb.SourceInfo{
|
||||
Filename: "mydockerfile",
|
||||
Data: []byte("filedata"),
|
||||
},
|
||||
Range: []*pb.Range{
|
||||
{Start: &pb.Position{Line: 2}, End: &pb.Position{Line: 4}},
|
||||
},
|
||||
Detail: [][]byte{[]byte("this is detail"), []byte("and more detail")},
|
||||
URL: "https://example.com",
|
||||
}))
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
wc := newWarningsCapture()
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, product, b, wc.status)
|
||||
require.NoError(t, err)
|
||||
|
||||
warnings := wc.wait()
|
||||
|
||||
require.Equal(t, 1, len(wc.vertexes))
|
||||
require.Equal(t, 1, len(warnings))
|
||||
|
||||
w := warnings[0]
|
||||
|
||||
require.Equal(t, "this is warning", string(w.Short))
|
||||
require.Equal(t, 2, len(w.Detail))
|
||||
require.Equal(t, "this is detail", string(w.Detail[0]))
|
||||
require.Equal(t, "and more detail", string(w.Detail[1]))
|
||||
require.Equal(t, "https://example.com", w.URL)
|
||||
require.Equal(t, 3, w.Level)
|
||||
_, ok := wc.vertexes[w.Vertex]
|
||||
require.True(t, ok)
|
||||
|
||||
require.Equal(t, "mydockerfile", w.SourceInfo.Filename)
|
||||
require.Equal(t, "filedata", string(w.SourceInfo.Data))
|
||||
require.Equal(t, 1, len(w.Range))
|
||||
require.Equal(t, int32(2), w.Range[0].Start.Line)
|
||||
require.Equal(t, int32(4), w.Range[0].End.Line)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
type warningsCapture struct {
|
||||
status chan *SolveStatus
|
||||
statusDone chan struct{}
|
||||
done chan struct{}
|
||||
warnings []*VertexWarning
|
||||
vertexes map[digest.Digest]struct{}
|
||||
}
|
||||
|
||||
func newWarningsCapture() *warningsCapture {
|
||||
w := &warningsCapture{
|
||||
status: make(chan *SolveStatus),
|
||||
statusDone: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
vertexes: map[digest.Digest]struct{}{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(w.statusDone)
|
||||
for {
|
||||
select {
|
||||
case st, ok := <-w.status:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, s := range st.Vertexes {
|
||||
w.vertexes[s.Digest] = struct{}{}
|
||||
}
|
||||
w.warnings = append(w.warnings, st.Warnings...)
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *warningsCapture) wait() []*VertexWarning {
|
||||
select {
|
||||
case <-w.statusDone:
|
||||
case <-time.After(10 * time.Second):
|
||||
close(w.done)
|
||||
}
|
||||
|
||||
<-w.statusDone
|
||||
return w.warnings
|
||||
}
|
||||
|
||||
type warningsListOutput []*VertexWarning
|
||||
|
||||
func (w warningsListOutput) String() string {
|
||||
if len(w) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
|
||||
for _, warn := range w {
|
||||
_, _ = b.Write(warn.Short)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,386 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/session/secrets/secretsprovider"
|
||||
"github.com/moby/buildkit/session/sshforward/sshprovider"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/iohelper"
|
||||
utilsystem "github.com/moby/buildkit/util/system"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tonistiigi/fsutil"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
// testClientGatewayContainerMounts is testing mounts derived from various
|
||||
// llb.States
|
||||
func testClientGatewayContainerMounts(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
tmpdir := integration.Tmpdir(t)
|
||||
|
||||
err = os.WriteFile(filepath.Join(tmpdir.Name, "local-file"), []byte("local"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
a := agent.NewKeyring()
|
||||
sockPath, err := makeSSHAgentSock(t, a)
|
||||
require.NoError(t, err)
|
||||
|
||||
ssh, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{
|
||||
ID: t.Name(),
|
||||
Paths: []string{sockPath},
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
product := "buildkit_test"
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
mounts := map[string]llb.State{
|
||||
"/": llb.Image("busybox:latest").Run(
|
||||
llb.Shlex("touch /root-file /cached/cache-file"),
|
||||
llb.AddMount("/cached", llb.Scratch(), llb.AsPersistentCacheDir(t.Name(), llb.CacheMountShared)),
|
||||
).Root(),
|
||||
"/foo": llb.Image("busybox:latest").Run(
|
||||
llb.Shlex("touch foo-file"),
|
||||
llb.Dir("/tmp"),
|
||||
llb.AddMount("/tmp", llb.Scratch()),
|
||||
).GetMount("/tmp"),
|
||||
"/local": llb.Local("mylocal"),
|
||||
// TODO How do we get a results.Ref for a cache mount, tmpfs mount
|
||||
}
|
||||
|
||||
containerMounts := []client.Mount{
|
||||
{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
},
|
||||
{
|
||||
Dest: "/foo",
|
||||
MountType: pb.MountType_BIND,
|
||||
},
|
||||
{
|
||||
Dest: "/local",
|
||||
MountType: pb.MountType_BIND,
|
||||
},
|
||||
{
|
||||
Dest: "/cached",
|
||||
MountType: pb.MountType_CACHE,
|
||||
CacheOpt: &pb.CacheOpt{
|
||||
ID: t.Name(),
|
||||
Sharing: pb.CacheSharingOpt_SHARED,
|
||||
},
|
||||
},
|
||||
{
|
||||
Dest: "/tmpfs",
|
||||
MountType: pb.MountType_TMPFS,
|
||||
},
|
||||
{
|
||||
Dest: "/run/secrets/mysecret",
|
||||
MountType: pb.MountType_SECRET,
|
||||
SecretOpt: &pb.SecretOpt{
|
||||
ID: "/run/secrets/mysecret",
|
||||
},
|
||||
},
|
||||
{
|
||||
Dest: sockPath,
|
||||
MountType: pb.MountType_SSH,
|
||||
SSHOpt: &pb.SSHOpt{
|
||||
ID: t.Name(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Fill in mount references.
|
||||
for i, m := range containerMounts {
|
||||
st := mounts[m.Dest]
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
containerMounts[i].Ref = r.Ref
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{Mounts: containerMounts})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pid1, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"sleep", "10"},
|
||||
Stderr: os.Stderr,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer pid1.Wait()
|
||||
|
||||
files := []struct {
|
||||
index int
|
||||
path string
|
||||
data []byte
|
||||
}{
|
||||
{0, "/root-file", []byte(nil)},
|
||||
{1, "/foo-file", []byte(nil)},
|
||||
{2, "/local-file", []byte(`local`)},
|
||||
{3, "/cache-file", []byte(nil)},
|
||||
}
|
||||
for _, file := range files {
|
||||
cpath := containerMounts[file.index].Dest + file.path
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-f", cpath},
|
||||
})
|
||||
require.NoError(t, err, "cannot start container to check for file: %s", cpath)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err, "process for checking file failed: %s", cpath)
|
||||
|
||||
_, err = ctr.StatFile(ctx, client.StatContainerRequest{
|
||||
StatRequest: client.StatRequest{
|
||||
Path: file.path,
|
||||
},
|
||||
MountIndex: file.index,
|
||||
})
|
||||
require.NoError(t, err, "stat file for %q on mount %d failed", file.path, file.index)
|
||||
|
||||
b, err := ctr.ReadFile(ctx, client.ReadContainerRequest{
|
||||
ReadRequest: client.ReadRequest{
|
||||
Filename: file.path,
|
||||
},
|
||||
MountIndex: file.index,
|
||||
})
|
||||
require.NoError(t, err, "read file for %q on mount %d failed", file.path, file.index)
|
||||
require.Equal(t, file.data, b)
|
||||
}
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-f", "/root-file"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-f", "/foo/foo-file"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-f", "/local/local-file"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-f", "/cached/cache-file"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-w", "/tmpfs"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
secretOutput := bytes.NewBuffer(nil)
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"cat", "/run/secrets/mysecret"},
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: secretOutput},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "foo-secret", secretOutput.String())
|
||||
|
||||
pid, err = ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"test", "-S", sockPath},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
return &client.Result{}, ctr.Release(ctx)
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
LocalMounts: map[string]fsutil.FS{
|
||||
"mylocal": tmpdir,
|
||||
},
|
||||
Session: []session.Attachable{
|
||||
ssh,
|
||||
secretsprovider.FromMap(map[string][]byte{
|
||||
"/run/secrets/mysecret": []byte("foo-secret"),
|
||||
}),
|
||||
},
|
||||
}, product, b, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), context.Canceled.Error())
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
// testClientGatewayContainerPlatformPATH is testing the correct default PATH
|
||||
// gets set for the requested platform
|
||||
func testClientGatewayContainerPlatformPATH(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
product := "buildkit_test"
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox:latest")
|
||||
def, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
Platform *pb.Platform
|
||||
Expected string
|
||||
}{{
|
||||
"default path",
|
||||
nil,
|
||||
utilsystem.DefaultPathEnvUnix,
|
||||
}, {
|
||||
"linux path",
|
||||
&pb.Platform{OS: "linux"},
|
||||
utilsystem.DefaultPathEnvUnix,
|
||||
}, {
|
||||
"windows path",
|
||||
&pb.Platform{OS: "windows"},
|
||||
utilsystem.DefaultPathEnvWindows,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
|
||||
Mounts: []client.Mount{{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
}},
|
||||
Platform: tt.Platform,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := bytes.NewBuffer(nil)
|
||||
pid1, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"/bin/sh", "-c", "echo -n $PATH"},
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: output},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = pid1.Wait()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.Expected, output.String())
|
||||
err = ctr.Release(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
return &client.Result{}, err
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testClientGatewayContainerSecretEnv(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
product := "buildkit_test"
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
mounts := map[string]llb.State{
|
||||
"/": llb.Image("busybox:latest"),
|
||||
}
|
||||
|
||||
var containerMounts []client.Mount
|
||||
for mountpoint, st := range mounts {
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
containerMounts = append(containerMounts, client.Mount{
|
||||
Dest: mountpoint,
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
})
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{Mounts: containerMounts})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"sh", "-c", "test $SOME_SECRET = foo-secret"},
|
||||
SecretEnv: []*pb.SecretEnv{
|
||||
{
|
||||
ID: "sekrit",
|
||||
Name: "SOME_SECRET",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = pid.Wait()
|
||||
require.NoError(t, err)
|
||||
|
||||
return &client.Result{}, ctr.Release(ctx)
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Session: []session.Attachable{
|
||||
secretsprovider.FromMap(map[string][]byte{
|
||||
"sekrit": []byte("foo-secret"),
|
||||
}),
|
||||
},
|
||||
}, product, b, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/moby/buildkit/util/iohelper"
|
||||
"github.com/moby/buildkit/util/testutil/echoserver"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testClientGatewayContainerExtraHosts(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
product := "buildkit_test"
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox")
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
|
||||
Mounts: []client.Mount{{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
}},
|
||||
ExtraHosts: []*pb.HostIP{{
|
||||
Host: "some.host",
|
||||
IP: "169.254.11.22",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"grep", "169.254.11.22\tsome.host", "/etc/hosts"},
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: stdout},
|
||||
Stderr: &iohelper.NopWriteCloser{Writer: stderr},
|
||||
})
|
||||
if err != nil {
|
||||
ctr.Release(ctx)
|
||||
return nil, err
|
||||
}
|
||||
defer ctr.Release(ctx)
|
||||
|
||||
err = pid.Wait()
|
||||
|
||||
t.Logf("Stdout: %q", stdout.String())
|
||||
t.Logf("Stderr: %q", stderr.String())
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
return &client.Result{}, nil
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testClientGatewayContainerHostNetworking(t *testing.T, sb integration.Sandbox, expectFail bool) {
|
||||
if os.Getenv("BUILDKIT_RUN_NETWORK_INTEGRATION_TESTS") == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
if sb.Rootless() && sb.Value("netmode") == defaultNetwork {
|
||||
// skip "default" network test for rootless, it always runs with "host" network
|
||||
// https://github.com/moby/buildkit/blob/v0.9.0/docs/rootless.md#known-limitations
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
product := "buildkit_test"
|
||||
|
||||
var allowedEntitlements []string
|
||||
netMode := pb.NetMode_UNSET
|
||||
if sb.Value("netmode") == hostNetwork {
|
||||
netMode = pb.NetMode_HOST
|
||||
allowedEntitlements = []string{entitlements.EntitlementNetworkHost.String()}
|
||||
if expectFail {
|
||||
allowedEntitlements = []string{}
|
||||
}
|
||||
}
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
s, err := echoserver.NewTestServer("foo")
|
||||
require.NoError(t, err)
|
||||
addrParts := strings.Split(s.Addr().String(), ":")
|
||||
port := addrParts[len(addrParts)-1]
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox")
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
|
||||
Mounts: []client.Mount{{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
}},
|
||||
NetMode: netMode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"/bin/sh", "-c", fmt.Sprintf("nc 127.0.0.1 %s | grep foo", port)},
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: stdout},
|
||||
Stderr: &iohelper.NopWriteCloser{Writer: stderr},
|
||||
})
|
||||
if err != nil {
|
||||
ctr.Release(ctx)
|
||||
return nil, err
|
||||
}
|
||||
defer ctr.Release(ctx)
|
||||
|
||||
err = pid.Wait()
|
||||
|
||||
t.Logf("Stdout: %q", stdout.String())
|
||||
t.Logf("Stderr: %q", stderr.String())
|
||||
|
||||
if netMode == pb.NetMode_HOST {
|
||||
if expectFail {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "network.host is not allowed")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
return &client.Result{}, nil
|
||||
}
|
||||
|
||||
solveOpts := SolveOpt{
|
||||
AllowedEntitlements: allowedEntitlements,
|
||||
}
|
||||
_, err = c.Build(ctx, solveOpts, product, b, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testClientGatewayContainerHostNetworkingAccess(t *testing.T, sb integration.Sandbox) {
|
||||
testClientGatewayContainerHostNetworking(t, sb, false)
|
||||
}
|
||||
|
||||
func testClientGatewayContainerHostNetworkingValidation(t *testing.T, sb integration.Sandbox) {
|
||||
testClientGatewayContainerHostNetworking(t, sb, true)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/moby/buildkit/util/iohelper"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testClientGatewayContainerInvalidSecurityMode(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureSecurityMode)
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
defer checkAllReleasable(t, c, sb, true)
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox:latest")
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
|
||||
Mounts: []client.Mount{{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ctr.Release(ctx)
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: []string{"sh", "-euc", `sec=$(awk '/^Seccomp:/{print $2}' /proc/self/status); echo "Seccomp=$sec"; test "$sec" = 0`},
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: stdout},
|
||||
Stderr: &iohelper.NopWriteCloser{Writer: stderr},
|
||||
SecurityMode: pb.SecurityMode(2),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = pid.Wait()
|
||||
|
||||
t.Logf("Stdout: %q", stdout.String())
|
||||
t.Logf("Stderr: %q", stderr.String())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", b, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid security mode")
|
||||
}
|
||||
|
||||
func testClientGatewayContainerSecurityMode(t *testing.T, sb integration.Sandbox, expectFail bool) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureSecurityMode)
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
product := "buildkit_test"
|
||||
|
||||
command := []string{"sh", "-c", `cat /proc/self/status | grep CapEff | cut -f 2`}
|
||||
mode := llb.SecurityModeSandbox
|
||||
var allowedEntitlements []string
|
||||
var assertCaps func(caps uint64)
|
||||
secMode := sb.Value("secmode")
|
||||
if secMode == securitySandbox {
|
||||
assertCaps = func(caps uint64) {
|
||||
/*
|
||||
$ capsh --decode=00000000a80425fb
|
||||
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
|
||||
cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
|
||||
*/
|
||||
require.Equal(t, uint64(0xa80425fb), caps)
|
||||
}
|
||||
allowedEntitlements = []string{}
|
||||
if expectFail {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
assertCaps = func(caps uint64) {
|
||||
/*
|
||||
$ capsh --decode=0000003fffffffff
|
||||
0x0000003fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
|
||||
cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,
|
||||
cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,
|
||||
cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,
|
||||
cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
|
||||
*/
|
||||
|
||||
// require that _at least_ minimum capabilities are granted
|
||||
require.Equal(t, uint64(0x3fffffffff), caps&0x3fffffffff)
|
||||
}
|
||||
mode = llb.SecurityModeInsecure
|
||||
allowedEntitlements = []string{entitlements.EntitlementSecurityInsecure.String()}
|
||||
if expectFail {
|
||||
allowedEntitlements = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox:latest")
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
|
||||
Mounts: []client.Mount{{
|
||||
Dest: "/",
|
||||
MountType: pb.MountType_BIND,
|
||||
Ref: r.Ref,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
|
||||
pid, err := ctr.Start(ctx, client.StartRequest{
|
||||
Args: command,
|
||||
Stdout: &iohelper.NopWriteCloser{Writer: stdout},
|
||||
Stderr: &iohelper.NopWriteCloser{Writer: stderr},
|
||||
SecurityMode: mode,
|
||||
})
|
||||
if err != nil {
|
||||
ctr.Release(ctx)
|
||||
return nil, err
|
||||
}
|
||||
defer ctr.Release(ctx)
|
||||
|
||||
err = pid.Wait()
|
||||
|
||||
t.Logf("Stdout: %q", stdout.String())
|
||||
t.Logf("Stderr: %q", stderr.String())
|
||||
|
||||
if expectFail {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "security.insecure is not allowed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
capsValue, err := strconv.ParseUint(strings.TrimSpace(stdout.String()), 16, 64)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertCaps(capsValue)
|
||||
|
||||
return &client.Result{}, nil
|
||||
}
|
||||
|
||||
solveOpts := SolveOpt{
|
||||
AllowedEntitlements: allowedEntitlements,
|
||||
}
|
||||
_, err = c.Build(ctx, solveOpts, product, b, nil)
|
||||
|
||||
if expectFail {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "security.insecure is not allowed")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
// testClientGatewayContainerSecurityModeCaps ensures that the correct security mode
|
||||
// is propagated to the gateway container
|
||||
func testClientGatewayContainerSecurityModeCaps(t *testing.T, sb integration.Sandbox) {
|
||||
testClientGatewayContainerSecurityMode(t, sb, false)
|
||||
}
|
||||
|
||||
func testClientGatewayContainerSecurityModeValidation(t *testing.T, sb integration.Sandbox) {
|
||||
testClientGatewayContainerSecurityMode(t, sb, true)
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/frontend/gateway/client"
|
||||
gatewayapi "github.com/moby/buildkit/frontend/gateway/pb"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/grpcerrors"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func testClientGatewayEmptyImageExec(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
target := registry + "/buildkit/testemptyimage:latest"
|
||||
|
||||
// push an empty image
|
||||
_, err = c.Build(sb.Context(), SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterImage,
|
||||
Attrs: map[string]string{
|
||||
"name": target,
|
||||
"push": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "", func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
return client.NewResult(), nil
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.Build(sb.Context(), SolveOpt{}, "", func(ctx context.Context, gw client.Client) (*client.Result, error) {
|
||||
// create an exec on that empty image (expected to fail, but not to panic)
|
||||
st := llb.Image(target).Run(
|
||||
llb.Args([]string{"echo", "hello"}),
|
||||
).Root()
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = gw.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
Evaluate: true,
|
||||
})
|
||||
require.ErrorContains(t, err, `process "echo hello" did not complete successfully`)
|
||||
return nil, nil
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testClientGatewayEmptySolve(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
r, err := c.Solve(ctx, client.SolveRequest{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
if r.Ref != nil || r.Refs != nil || r.Metadata != nil {
|
||||
return nil, errors.Errorf("got unexpected non-empty result %+v", r)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "", b, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testClientGatewayFailedSolve(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
return nil, errors.New("expected to fail")
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "", b, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "expected to fail")
|
||||
}
|
||||
|
||||
func testClientGatewayNilResult(t *testing.T, sb integration.Sandbox) {
|
||||
workers.CheckFeatureCompat(t, sb, workers.FeatureMergeDiff)
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
st := llb.Image("busybox:latest")
|
||||
diff := llb.Diff(st, st)
|
||||
def, err := diff.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
Evaluate: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ref, err := res.SingleRef()
|
||||
require.NoError(t, err)
|
||||
|
||||
dirEnts, err := ref.ReadDir(ctx, client.ReadDirRequest{
|
||||
Path: "/",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dirEnts, 0)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err = c.Build(sb.Context(), SolveOpt{}, "", b, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testClientGatewaySolve(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
product := "buildkit_test"
|
||||
optKey := "test-string"
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
if c.BuildOpts().Product != product {
|
||||
return nil, errors.Errorf("expected product %q, got %q", product, c.BuildOpts().Product)
|
||||
}
|
||||
opts := c.BuildOpts().Opts
|
||||
testStr, ok := opts[optKey]
|
||||
if !ok {
|
||||
return nil, errors.Errorf(`build option %q missing`, optKey)
|
||||
}
|
||||
|
||||
run := llb.Image("busybox:latest").Run(
|
||||
llb.ReadonlyRootFS(),
|
||||
llb.Args([]string{"/bin/sh", "-ec", `echo -n '` + testStr + `' > /out/foo`}),
|
||||
)
|
||||
st := run.AddMount("/out", llb.Scratch())
|
||||
|
||||
def, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal state")
|
||||
}
|
||||
|
||||
r, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to solve")
|
||||
}
|
||||
|
||||
read, err := r.Ref.ReadFile(ctx, client.ReadRequest{
|
||||
Filename: "/foo",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read result")
|
||||
}
|
||||
if testStr != string(read) {
|
||||
return nil, errors.Errorf("read back %q, expected %q", string(read), testStr)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
testStr := "This is a test"
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
FrontendAttrs: map[string]string{
|
||||
optKey: testStr,
|
||||
},
|
||||
}, product, b, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
read, err := os.ReadFile(filepath.Join(tmpdir, "foo"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testStr, string(read))
|
||||
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testClientSlowCacheRootfsRef(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
|
||||
id := identity.NewID()
|
||||
input := llb.Scratch().File(
|
||||
llb.Mkdir("/found", 0o700).
|
||||
Mkfile("/found/data", 0o600, []byte(id)),
|
||||
)
|
||||
|
||||
st := llb.Image("busybox:latest").Run(
|
||||
llb.Shlexf("echo hello"),
|
||||
// Only readonly mounts trigger slow cache errors.
|
||||
llb.AddMount("/src", input, llb.SourcePath("/notfound"), llb.Readonly),
|
||||
).Root()
|
||||
|
||||
def1, err := st.Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
res1, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def1.ToPB(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ref1, err := res1.SingleRef()
|
||||
require.NoError(t, err)
|
||||
|
||||
// First stat should error because unlazy-ing the reference causes an error
|
||||
// in CalcSlowCache.
|
||||
_, err = ref1.StatFile(ctx, client.StatRequest{
|
||||
Path: ".",
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
def2, err := llb.Image("busybox:latest").Marshal(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
res2, err := c.Solve(ctx, client.SolveRequest{
|
||||
Definition: def2.ToPB(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ref2, err := res2.SingleRef()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second stat should not error because the rootfs for `busybox` should not
|
||||
// have been released.
|
||||
_, err = ref2.StatFile(ctx, client.StatRequest{
|
||||
Path: ".",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return res2, nil
|
||||
}
|
||||
|
||||
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", b, nil)
|
||||
require.NoError(t, err)
|
||||
checkAllReleasable(t, c, sb, true)
|
||||
}
|
||||
|
||||
func testNoBuildID(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
g := gatewayapi.NewLLBBridgeClient(c.conn)
|
||||
_, err = g.Ping(ctx, &gatewayapi.PingRequest{})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no buildid found in context")
|
||||
}
|
||||
|
||||
func testUnknownBuildID(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
|
||||
ctx := sb.Context()
|
||||
|
||||
c, err := New(ctx, sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
g := c.gatewayClientForBuild(t.Name() + identity.NewID())
|
||||
_, err = g.Ping(ctx, &gatewayapi.PingRequest{})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no such job")
|
||||
require.Equal(t, codes.NotFound, grpcerrors.Code(err))
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
solvererrdefs "github.com/moby/buildkit/solver/errdefs"
|
||||
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
|
||||
opspb "github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/sourcepolicy"
|
||||
sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb"
|
||||
"github.com/moby/buildkit/sourcepolicy/policysession"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
@@ -1580,3 +1582,187 @@ func tamperDigestHex(dgst digest.Digest) digest.Digest {
|
||||
}
|
||||
return digest.NewDigestFromEncoded(dgst.Algorithm(), string(hexPart))
|
||||
}
|
||||
|
||||
func testSourcePolicy(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
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.Image("busybox:1.34.1-uclibc").File(
|
||||
llb.Copy(llb.HTTP("https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md"),
|
||||
"README.md", "README.md"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
})
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
srcPol *sourcepolicypb.Policy
|
||||
expectedErr string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
// Valid
|
||||
srcPol: &sourcepolicypb.Policy{
|
||||
Rules: []*sourcepolicypb.Rule{
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_CONVERT,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "docker-image://docker.io/library/busybox:1.34.1-uclibc",
|
||||
},
|
||||
Updates: &sourcepolicypb.Update{
|
||||
Identifier: "docker-image://docker.io/library/busybox:1.34.1-uclibc@sha256:3614ca5eacf0a3a1bcc361c939202a974b4902b9334ff36eb29ffe9011aaad83",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_CONVERT,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
|
||||
},
|
||||
Updates: &sourcepolicypb.Update{
|
||||
Identifier: "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
|
||||
Attrs: map[string]string{"http.checksum": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
// Invalid docker-image source
|
||||
srcPol: &sourcepolicypb.Policy{
|
||||
Rules: []*sourcepolicypb.Rule{
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_CONVERT,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "docker-image://docker.io/library/busybox:1.34.1-uclibc",
|
||||
},
|
||||
Updates: &sourcepolicypb.Update{
|
||||
Identifier: "docker-image://docker.io/library/busybox:1.34.1-uclibc@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // invalid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "docker.io/library/busybox:1.34.1-uclibc@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: not found",
|
||||
},
|
||||
{
|
||||
// Invalid http source
|
||||
srcPol: &sourcepolicypb.Policy{
|
||||
Rules: []*sourcepolicypb.Rule{
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_CONVERT,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
|
||||
},
|
||||
Updates: &sourcepolicypb.Update{
|
||||
Attrs: map[string]string{opspb.AttrHTTPChecksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, // invalid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "digest mismatch sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53: sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
_, err = c.Build(sb.Context(), SolveOpt{SourcePolicy: tc.srcPol}, "", frontend, nil)
|
||||
if tc.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Frontend policies", func(t *testing.T) {
|
||||
t.Run("deny http", func(t *testing.T) {
|
||||
denied := "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md"
|
||||
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
st := llb.Image("busybox:1.34.1-uclibc").File(
|
||||
llb.Copy(llb.HTTP(denied),
|
||||
"README.md", "README.md"))
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
SourcePolicies: []*sourcepolicypb.Policy{{
|
||||
Rules: []*sourcepolicypb.Rule{
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_DENY,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: denied,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
_, err = c.Build(sb.Context(), SolveOpt{}, "", frontend, nil)
|
||||
require.ErrorContains(t, err, sourcepolicy.ErrSourceDenied.Error())
|
||||
})
|
||||
t.Run("resolve image config", func(t *testing.T) {
|
||||
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
const (
|
||||
origRef = "docker.io/library/busybox:1.34.1-uclibc"
|
||||
updatedRef = "docker.io/library/busybox:latest"
|
||||
)
|
||||
pol := []*sourcepolicypb.Policy{
|
||||
{
|
||||
Rules: []*sourcepolicypb.Rule{
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_DENY,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "*",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_ALLOW,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "docker-image://" + updatedRef + "*",
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: sourcepolicypb.PolicyAction_CONVERT,
|
||||
Selector: &sourcepolicypb.Selector{
|
||||
Identifier: "docker-image://" + origRef,
|
||||
},
|
||||
Updates: &sourcepolicypb.Update{
|
||||
Identifier: "docker-image://" + updatedRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ref, dgst, _, err := c.ResolveImageConfig(ctx, origRef, sourceresolver.Opt{
|
||||
SourcePolicies: pol,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.Equal(t, updatedRef, ref)
|
||||
st := llb.Image(ref + "@" + dgst.String())
|
||||
def, err := st.Marshal(sb.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Solve(ctx, gateway.SolveRequest{
|
||||
Definition: def.ToPB(),
|
||||
SourcePolicies: pol,
|
||||
})
|
||||
}
|
||||
_, err = c.Build(sb.Context(), SolveOpt{}, "", frontend, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user