Files
buildkit/client/gateway_container_exec_test.go
Tonis Tiigi 38f8536ee3 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>
2026-07-06 22:19:20 -07:00

1273 lines
32 KiB
Go

package client
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"syscall"
"testing"
"time"
"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/solver/errdefs"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
// testClientGatewayContainerCancelExecTty is testing the tty shuts down cleanly
// on context.Cancel
func testClientGatewayContainerCancelExecTty(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"
inputR, inputW := io.Pipe()
output := bytes.NewBuffer(nil)
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
ctx, timeout := context.WithTimeoutCause(ctx, 10*time.Second, nil)
defer timeout()
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,
}},
})
require.NoError(t, err)
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
defer pid1.Wait()
defer ctr.Release(ctx)
execCtx, cancel := context.WithCancelCause(ctx)
defer func() { cancel(errors.WithStack(context.Canceled)) }()
prompt := newTestPrompt(execCtx, t, inputW, output)
pid2, err := ctr.Start(execCtx, client.StartRequest{
Args: []string{"sh"},
Tty: true,
Stdin: inputR,
Stdout: &iohelper.NopWriteCloser{Writer: output},
Stderr: &iohelper.NopWriteCloser{Writer: output},
Env: []string{fmt.Sprintf("PS1=%s", prompt.String())},
})
require.NoError(t, err)
prompt.SendExpect("echo hi", "hi")
cancel(errors.WithStack(context.Canceled))
err = pid2.Wait()
require.ErrorIs(t, err, context.Canceled)
return &client.Result{}, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
require.Contains(t, err.Error(), context.Canceled.Error())
inputW.Close()
inputR.Close()
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerCancelOnRelease is testing that all running
// processes are terminated when the container is released.
func testClientGatewayContainerCancelOnRelease(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)
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
}
start := time.Now()
defer func() {
// ensure pid1 and pid2 exit from cancel before the 10s sleep
// exits naturally
require.WithinDuration(t, start, time.Now(), 10*time.Second)
}()
// background pid1 process that starts container
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
pid2, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
ctr.Release(ctx)
err = pid1.Wait()
require.Contains(t, err.Error(), context.Canceled.Error())
err = pid2.Wait()
require.Contains(t, err.Error(), context.Canceled.Error())
return &client.Result{}, nil
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerCancelPID1Tty is testing that the tty will cleanly
// shutdown on context cancel
func testClientGatewayContainerCancelPID1Tty(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"
inputR, inputW := io.Pipe()
output := bytes.NewBuffer(nil)
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
ctx, cancel := context.WithTimeoutCause(ctx, 10*time.Second, nil)
defer cancel()
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,
}},
})
require.NoError(t, err)
defer ctr.Release(ctx)
prompt := newTestPrompt(ctx, t, inputW, output)
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh"},
Tty: true,
Stdin: inputR,
Stdout: &iohelper.NopWriteCloser{Writer: output},
Stderr: &iohelper.NopWriteCloser{Writer: output},
Env: []string{fmt.Sprintf("PS1=%s", prompt.String())},
})
require.NoError(t, err)
prompt.SendExpect("echo hi", "hi")
cancel()
err = pid1.Wait()
require.ErrorIs(t, err, context.Canceled)
return &client.Result{}, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
inputW.Close()
inputR.Close()
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerExecPipe is testing the ability to pipe multiple
// process together all started via `Exec` into the same container.
// We are mimicing: `echo testing | cat | cat > /tmp/foo && cat /tmp/foo`
func testClientGatewayContainerExecPipe(t *testing.T, sb integration.Sandbox) {
testClientGatewayContainerExecPipeWithCleanup(t, sb, func(ctx context.Context, ctr client.Container, pid1 client.ContainerProcess, stdin *io.PipeWriter) {
stdin.Close()
pid1.Wait()
ctr.Release(context.WithoutCancel(ctx))
})
}
// testClientGatewayContainerExecPipeRelease verifies that releasing the
// container while pid1 still has an open stdin reader does not leave runc
// blocked on the stdin copy.
func testClientGatewayContainerExecPipeRelease(t *testing.T, sb integration.Sandbox) {
testClientGatewayContainerExecPipeWithCleanup(t, sb, func(ctx context.Context, ctr client.Container, pid1 client.ContainerProcess, _ *io.PipeWriter) {
ctr.Release(context.WithoutCancel(ctx))
pid1.Wait()
})
}
// testClientGatewayContainerExecPipeSignalKill verifies that SIGKILL delivered
// via the process Signal channel unblocks runc when pid1 still has an open
// stdin reader.
func testClientGatewayContainerExecPipeSignalKill(t *testing.T, sb integration.Sandbox) {
testClientGatewayContainerExecPipeWithCleanup(t, sb, func(ctx context.Context, ctr client.Container, pid1 client.ContainerProcess, _ *io.PipeWriter) {
pid1.Signal(ctx, syscall.SIGKILL)
pid1.Wait()
ctr.Release(context.WithoutCancel(ctx))
})
}
func testClientGatewayContainerExecPipeWithCleanup(t *testing.T, sb integration.Sandbox, cleanup func(ctx context.Context, ctr client.Container, pid1 client.ContainerProcess, stdin *io.PipeWriter)) {
requiresLinux(t)
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
product := "buildkit_test"
output := bytes.NewBuffer(nil)
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
}
// background pid1 process that starts container
pid1StdinR, pid1StdinW := io.Pipe()
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat"},
Stdin: pid1StdinR,
})
if err != nil {
ctr.Release(context.WithoutCancel(ctx))
return nil, err
}
defer cleanup(ctx, ctr, pid1, pid1StdinW)
// first part is `echo testing | cat`
stdin2 := bytes.NewBuffer([]byte("testing"))
stdin3, stdout2 := io.Pipe()
pid2, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat"},
Cwd: "/",
Tty: false,
Stdin: io.NopCloser(stdin2),
Stdout: stdout2,
})
if err != nil {
return nil, err
}
// next part is: `| cat > /tmp/test`
pid3, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh", "-c", "cat > /tmp/test"},
Stdin: stdin3,
})
if err != nil {
return nil, err
}
err = pid2.Wait()
if err != nil {
stdout2.Close()
return nil, err
}
err = stdout2.Close()
if err != nil {
return nil, err
}
err = pid3.Wait()
if err != nil {
return nil, err
}
err = stdin3.Close()
if err != nil {
return nil, err
}
pid4, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat", "/tmp/test"},
Stdout: &iohelper.NopWriteCloser{Writer: output},
})
if err != nil {
return nil, err
}
err = pid4.Wait()
if err != nil {
return nil, err
}
return &client.Result{}, nil
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.NoError(t, err)
require.Equal(t, "testing", output.String())
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerExecTty is testing that we can get a tty via
// executor.Exec (secondary process)
func testClientGatewayContainerExecTty(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"
inputR, inputW := io.Pipe()
output := bytes.NewBuffer(nil)
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
ctx, timeout := context.WithTimeoutCause(ctx, 10*time.Second, nil)
defer timeout()
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,
}},
})
require.NoError(t, err)
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
defer pid1.Wait()
defer ctr.Release(ctx)
prompt := newTestPrompt(ctx, t, inputW, output)
pid2, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh"},
Tty: true,
Stdin: inputR,
Stdout: &iohelper.NopWriteCloser{Writer: output},
Stderr: &iohelper.NopWriteCloser{Writer: output},
Env: []string{fmt.Sprintf("PS1=%s", prompt.String())},
})
require.NoError(t, err)
err = pid2.Resize(ctx, client.WinSize{Rows: 40, Cols: 80})
require.NoError(t, err)
prompt.SendExpect("ttysize", "80 40")
prompt.Send("cd /tmp")
prompt.SendExpect("pwd", "/tmp")
prompt.Send("echo foobar > newfile")
prompt.SendExpect("cat /tmp/newfile", "foobar")
err = pid2.Resize(ctx, client.WinSize{Rows: 60, Cols: 100})
require.NoError(t, err)
prompt.SendExpect("ttysize", "100 60")
prompt.SendExit(99)
err = pid2.Wait()
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(99), exitError.ExitCode)
return &client.Result{}, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(99), exitError.ExitCode)
require.Regexp(t, "exit code: 99", err.Error())
inputW.Close()
inputR.Close()
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerPID1Exit is testing that all process started
// via `Exec` are shutdown when the primary pid1 process exits
func testClientGatewayContainerPID1Exit(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)
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)
start := time.Now()
defer func() {
// ensure pid1 and pid2 exits from cancel before the 10s sleep
// exits naturally
require.WithinDuration(t, start, time.Now(), 10*time.Second)
// assert this test ran for at least one second for pid1
lapse := time.Since(start)
require.Greater(t, lapse.Seconds(), float64(1))
}()
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "1"},
})
require.NoError(t, err)
defer pid1.Wait()
pid2, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
return &client.Result{}, pid2.Wait()
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(137), exitError.ExitCode)
// `exit code: 137` (ie sigkill)
require.Regexp(t, "exit code: 137", err.Error())
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerPID1Fail is testing clean shutdown and release
// of resources when the primary pid1 exits with non-zero exit status
func testClientGatewayContainerPID1Fail(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)
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
}
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh", "-c", "exit 99"},
})
if err != nil {
ctr.Release(ctx)
return nil, err
}
defer ctr.Release(ctx)
err = pid1.Wait()
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(99), exitError.ExitCode)
return nil, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerPID1Tty is testing that we can get a tty via
// a container pid1, executor.Run
func testClientGatewayContainerPID1Tty(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"
inputR, inputW := io.Pipe()
output := bytes.NewBuffer(nil)
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
ctx, timeout := context.WithTimeoutCause(ctx, 10*time.Second, nil)
defer timeout()
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,
}},
})
require.NoError(t, err)
defer ctr.Release(ctx)
prompt := newTestPrompt(ctx, t, inputW, output)
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh"},
Tty: true,
Stdin: inputR,
Stdout: &iohelper.NopWriteCloser{Writer: output},
Stderr: &iohelper.NopWriteCloser{Writer: output},
Env: []string{fmt.Sprintf("PS1=%s", prompt.String())},
})
require.NoError(t, err)
err = pid1.Resize(ctx, client.WinSize{Rows: 40, Cols: 80})
require.NoError(t, err)
prompt.SendExpect("ttysize", "80 40")
prompt.Send("cd /tmp")
prompt.SendExpect("pwd", "/tmp")
prompt.Send("echo foobar > newfile")
prompt.SendExpect("cat /tmp/newfile", "foobar")
err = pid1.Resize(ctx, client.WinSize{Rows: 60, Cols: 100})
require.NoError(t, err)
prompt.SendExpect("ttysize", "100 60")
prompt.SendExit(99)
err = pid1.Wait()
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(99), exitError.ExitCode)
return &client.Result{}, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
inputW.Close()
inputR.Close()
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayContainerSignal is testing that we can send a signal
func testClientGatewayContainerSignal(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) {
ctx, timeout := context.WithTimeoutCause(ctx, 10*time.Second, nil)
defer timeout()
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")
}
ctr1, err := c.NewContainer(ctx, client.NewContainerRequest{
Mounts: []client.Mount{{
Dest: "/",
MountType: pb.MountType_BIND,
Ref: r.Ref,
}},
})
require.NoError(t, err)
defer ctr1.Release(ctx)
pid1, err := ctr1.Start(ctx, client.StartRequest{
Args: []string{"sh", "-c", `trap 'kill $(jobs -p); exit 99' HUP; sleep 10 & wait`},
})
require.NoError(t, err)
// allow for the shell script to setup the trap before we signal it
time.Sleep(time.Second)
err = pid1.Signal(ctx, syscall.SIGHUP)
require.NoError(t, err)
err = pid1.Wait()
var exitError *gatewayapi.ExitError
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(99), exitError.ExitCode)
// Now try again to signal an exec process
ctr2, err := c.NewContainer(ctx, client.NewContainerRequest{
Mounts: []client.Mount{{
Dest: "/",
MountType: pb.MountType_BIND,
Ref: r.Ref,
}},
})
require.NoError(t, err)
defer ctr2.Release(ctx)
pid1, err = ctr2.Start(ctx, client.StartRequest{
Args: []string{"sleep", "10"},
})
require.NoError(t, err)
pid2, err := ctr2.Start(ctx, client.StartRequest{
Args: []string{"sh", "-c", `trap 'kill $(jobs -p); exit 111' INT; sleep 10 & wait`},
})
require.NoError(t, err)
// allow for the shell script to setup the trap before we signal it
time.Sleep(time.Second)
err = pid2.Signal(ctx, syscall.SIGINT)
require.NoError(t, err)
err = pid2.Wait()
require.ErrorAs(t, err, &exitError)
require.Equal(t, uint32(111), exitError.ExitCode)
pid1.Signal(ctx, syscall.SIGKILL)
pid1.Wait()
return &client.Result{}, err
}
_, err = c.Build(ctx, SolveOpt{}, product, b, nil)
require.Error(t, err)
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayExecError is testing gateway exec to recreate the container
// process for a failed execop.
func testClientGatewayExecError(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()
tests := []struct {
Name string
State llb.State
NumMounts int
Paths []string
}{{
"only rootfs",
llb.Image("busybox:latest").Run(
llb.Shlexf(`sh -c "echo %s > /data && fail"`, id),
).Root(),
1,
[]string{"/data"},
}, {
"rootfs and readwrite scratch mount",
llb.Image("busybox:latest").Run(
llb.Shlexf(`sh -c "echo %s > /data && echo %s > /rw/data && fail"`, id, id),
llb.AddMount("/rw", llb.Scratch()),
).Root(),
2,
[]string{"/data", "/rw/data"},
}, {
"rootfs and readwrite mount",
llb.Image("busybox:latest").Run(
llb.Shlexf(`sh -c "echo %s > /data && echo %s > /rw/data && fail"`, id, id),
llb.AddMount("/rw", llb.Scratch().File(llb.Mkfile("foo", 0o700, []byte(id)))),
).Root(),
2,
[]string{"/data", "/rw/data", "/rw/foo"},
}, {
"rootfs and readonly scratch mount",
llb.Image("busybox:latest").Run(
llb.Shlexf(`sh -c "echo %s > /data && echo %s > /readonly/foo"`, id, id),
llb.AddMount("/readonly", llb.Scratch(), llb.Readonly),
).Root(),
2,
[]string{"/data"},
}, {
"rootfs and readwrite force no output mount",
llb.Image("busybox:latest").Run(
llb.Shlexf(`sh -c "echo %s > /data && echo %s > /rw/data && fail"`, id, id),
llb.AddMount(
"/rw",
llb.Scratch().File(llb.Mkfile("foo", 0o700, []byte(id))),
llb.ForceNoOutput,
),
).Root(),
2,
[]string{"/data", "/rw/data", "/rw/foo"},
}}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
def, err := tt.State.Marshal(ctx)
require.NoError(t, err)
_, solveErr := c.Solve(ctx, client.SolveRequest{
Evaluate: true,
Definition: def.ToPB(),
})
require.Error(t, solveErr)
var se *errdefs.SolveError
require.ErrorAs(t, solveErr, &se)
require.Len(t, se.InputIDs, tt.NumMounts)
require.Len(t, se.MountIDs, tt.NumMounts)
op := se.Op
require.NotNil(t, op)
require.NotNil(t, op.Op)
opExec, ok := se.Op.Op.(*pb.Op_Exec)
require.True(t, ok)
exec := opExec.Exec
var mounts []client.Mount
for i, mnt := range exec.Mounts {
mounts = append(mounts, client.Mount{
Selector: mnt.Selector,
Dest: mnt.Dest,
ResultID: se.MountIDs[i],
Readonly: mnt.Readonly,
MountType: mnt.MountType,
CacheOpt: mnt.CacheOpt,
SecretOpt: mnt.SecretOpt,
SSHOpt: mnt.SSHOpt,
})
}
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
Mounts: mounts,
NetMode: exec.Network,
Platform: op.Platform,
Constraints: op.Constraints,
})
require.NoError(t, err)
defer ctr.Release(ctx)
inputR, inputW := io.Pipe()
defer inputW.Close()
defer inputR.Close()
pid1Output := bytes.NewBuffer(nil)
prompt := newTestPrompt(ctx, t, inputW, pid1Output)
pid1, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"sh"},
Tty: true,
Stdin: inputR,
Stdout: &iohelper.NopWriteCloser{Writer: pid1Output},
Stderr: &iohelper.NopWriteCloser{Writer: pid1Output},
Env: []string{fmt.Sprintf("PS1=%s", prompt.String())},
})
require.NoError(t, err)
meta := exec.Meta
for _, p := range tt.Paths {
output := bytes.NewBuffer(nil)
proc, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat", p},
Env: meta.Env,
User: meta.User,
Cwd: meta.Cwd,
Stdout: &iohelper.NopWriteCloser{Writer: output},
SecurityMode: exec.Security,
})
require.NoError(t, err)
err = proc.Wait()
require.NoError(t, err)
require.Equal(t, id, strings.TrimSpace(output.String()))
}
prompt.SendExit(0)
err = pid1.Wait()
require.NoError(t, err)
})
}
return client.NewResult(), nil
}
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", b, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, true)
}
// testClientGatewayExecFileActionError is testing gateway exec into the modified
// mount of a failed fileop during a solve.
func testClientGatewayExecFileActionError(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) {
st := llb.Image("busybox:latest")
def, err := st.Marshal(ctx)
require.NoError(t, err)
res, err := c.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
require.NoError(t, err)
debugfs, err := res.SingleRef()
require.NoError(t, err)
id := identity.NewID()
tests := []struct {
Name string
State llb.State
NumInputs int
NumOutputs int
Path string
}{{
"mkfile",
llb.Scratch().File(
llb.Mkdir("/found", 0o700).
Mkfile("/found/foo", 0o600, []byte(id)).
Mkfile("/notfound/foo", 0o600, []byte(id)),
),
0, 3, "/input/found/foo",
}, {
"copy from input",
llb.Image("busybox").File(
llb.Copy(
llb.Scratch().File(
llb.Mkdir("/foo", 0o600).Mkfile("/foo/bar", 0o700, []byte(id)),
),
"/foo/bar",
"/notfound/baz",
),
),
2, 1, "/secondary/foo/bar",
}, {
"copy from action",
llb.Image("busybox").File(
llb.Copy(
llb.Mkdir("/foo", 0o600).Mkfile("/foo/bar", 0o700, []byte(id)).WithState(llb.Scratch()),
"/foo/bar",
"/notfound/baz",
),
),
1, 3, "/secondary/foo/bar",
}}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
def, err := tt.State.Marshal(ctx)
require.NoError(t, err)
_, err = c.Solve(ctx, client.SolveRequest{
Evaluate: true,
Definition: def.ToPB(),
})
require.Error(t, err)
var se *errdefs.SolveError
require.ErrorAs(t, err, &se)
require.Len(t, se.InputIDs, tt.NumInputs)
// There is one output for every action in the fileop that failed.
require.Len(t, se.MountIDs, tt.NumOutputs)
op, ok := se.Op.Op.(*pb.Op_File)
require.True(t, ok)
subject, ok := se.Subject.(*errdefs.Solve_File)
require.True(t, ok)
// Retrieve the action that failed from the sbuject.
idx := subject.File.Index
require.Less(t, int(idx), len(op.File.Actions))
action := op.File.Actions[idx]
// The output for a file action is mapped by its index.
inputID := se.MountIDs[idx]
var secondaryID string
if action.SecondaryInput != -1 {
// If the secondary input is a result from another exec, it will be one
// of the input IDs, otherwise it's a intermediary mutable from another
// action in the same fileop.
if int(action.SecondaryInput) < len(se.InputIDs) {
secondaryID = se.InputIDs[action.SecondaryInput]
} else {
secondaryID = se.MountIDs[int(action.SecondaryInput)-len(se.InputIDs)]
}
}
mounts := []client.Mount{{
Dest: "/",
MountType: pb.MountType_BIND,
Ref: debugfs,
}, {
Dest: "/input",
MountType: pb.MountType_BIND,
ResultID: inputID,
}}
if secondaryID != "" {
mounts = append(mounts, client.Mount{
Dest: "/secondary",
MountType: pb.MountType_BIND,
ResultID: secondaryID,
})
}
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{Mounts: mounts})
require.NoError(t, err)
// Verify that the randomly generated data can be found in a mutable ref
// created by the actions that have succeeded.
output := bytes.NewBuffer(nil)
proc, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat", tt.Path},
Stdout: &iohelper.NopWriteCloser{Writer: output},
})
require.NoError(t, err)
err = proc.Wait()
require.NoError(t, err)
require.Equal(t, id, strings.TrimSpace(output.String()))
err = ctr.Release(ctx)
require.NoError(t, err)
})
}
return client.NewResult(), nil
}
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", b, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, true)
}
// testClientGatewaySlowCacheExecError is testing gateway exec into the ref
// that failed to mount during an execop.
func testClientGatewaySlowCacheExecError(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
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()
def, err := st.Marshal(ctx)
require.NoError(t, err)
b := func(ctx context.Context, c client.Client) (*client.Result, error) {
_, solveErr := c.Solve(ctx, client.SolveRequest{
Evaluate: true,
Definition: def.ToPB(),
})
require.Error(t, solveErr)
var se *errdefs.SolveError
require.ErrorAs(t, solveErr, &se)
_, ok := se.Op.Op.(*pb.Op_Exec)
require.True(t, ok)
_, ok = se.Subject.(*errdefs.Solve_Cache)
require.True(t, ok)
// Slow cache errors should only have exactly one input and no outputs.
require.Len(t, se.InputIDs, 1)
require.Len(t, se.MountIDs, 0)
st := llb.Image("busybox:latest")
def, err := st.Marshal(ctx)
require.NoError(t, err)
res, err := c.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
require.NoError(t, err)
ref, err := res.SingleRef()
require.NoError(t, err)
ctr, err := c.NewContainer(ctx, client.NewContainerRequest{
Mounts: []client.Mount{{
Dest: "/",
MountType: pb.MountType_BIND,
Ref: ref,
}, {
Dest: "/problem",
MountType: pb.MountType_BIND,
ResultID: se.InputIDs[0],
}},
})
require.NoError(t, err)
defer ctr.Release(ctx)
output := bytes.NewBuffer(nil)
proc, err := ctr.Start(ctx, client.StartRequest{
Args: []string{"cat", "/problem/found/data"},
Stdout: &iohelper.NopWriteCloser{Writer: output},
})
require.NoError(t, err)
err = proc.Wait()
require.NoError(t, err)
require.Equal(t, id, strings.TrimSpace(output.String()))
return client.NewResult(), nil
}
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", b, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, true)
}
type testPrompt struct {
ctx context.Context
t *testing.T
output *bytes.Buffer
input io.Writer
prompt string
pos int
}
func (p *testPrompt) String() string { return p.prompt }
func (p *testPrompt) SendExit(status int) {
p.input.Write(fmt.Appendf(nil, "exit %d\n", status))
}
func (p *testPrompt) Send(cmd string) {
p.input.Write([]byte(cmd + "\n"))
p.wait(p.prompt)
}
func (p *testPrompt) SendExpect(cmd, expected string) {
for {
p.input.Write([]byte(cmd + "\n"))
response := p.wait(p.prompt)
if strings.Contains(response, expected) {
return
}
}
}
func (p *testPrompt) wait(msg string) string {
for {
newOutput := p.output.String()[p.pos:]
if strings.Contains(newOutput, msg) {
p.pos += len(newOutput)
return newOutput
}
select {
case <-p.ctx.Done():
p.t.Logf("Output at timeout: %s", p.output.String())
p.t.Fatalf("Timeout waiting for %q", msg)
case <-time.After(100 * time.Millisecond):
}
}
}
func newTestPrompt(ctx context.Context, t *testing.T, input io.Writer, output *bytes.Buffer) *testPrompt {
return &testPrompt{
ctx: ctx,
t: t,
input: input,
output: output,
prompt: "% ",
}
}