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:
Tonis Tiigi committed 2026-07-06 22:19:20 -07:00
1 parent c130a532e6
commit 38f8536ee3
29 files changed
+18119 -17555

No files matched your search

-2618
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+130
View File
@@ -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
}
}
}
+16
View File
@@ -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)
}
+743
View File
@@ -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
+910
View File
@@ -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
+936
View File
@@ -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)
}
+431
View File
@@ -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")
}
+706
View File
@@ -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
}
+877
View File
@@ -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)
}
+365
View File
@@ -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
}
}
+400
View File
@@ -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)
}
+590
View File
@@ -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
+437
View File
@@ -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{}
)
+606
View File
@@ -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)
}
+199
View File
@@ -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
}
+132
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+328
View File
@@ -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")
}
+155
View File
@@ -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
+386
View File
@@ -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)
}
+201
View File
@@ -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)
}
+221
View File
@@ -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)
}
+317
View File
@@ -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))
}
+186
View File
@@ -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)
})
})
}