mirror of
https://github.com/moby/buildkit.git
synced 2026-08-06 15:50:50 +00:00
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>
1273 lines
32 KiB
Go
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: "% ",
|
|
}
|
|
}
|