diff --git a/client/build_test.go b/client/build_test.go deleted file mode 100644 index 2e9e2d166..000000000 --- a/client/build_test.go +++ /dev/null @@ -1,2618 +0,0 @@ -package client - -import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/http/httputil" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "testing" - "time" - - "github.com/moby/buildkit/client/llb" - "github.com/moby/buildkit/client/llb/sourceresolver" - "github.com/moby/buildkit/frontend/gateway/client" - gatewayapi "github.com/moby/buildkit/frontend/gateway/pb" - "github.com/moby/buildkit/identity" - "github.com/moby/buildkit/session" - sessionauth "github.com/moby/buildkit/session/auth" - "github.com/moby/buildkit/session/secrets/secretsprovider" - "github.com/moby/buildkit/session/sshforward/sshprovider" - "github.com/moby/buildkit/solver/errdefs" - "github.com/moby/buildkit/solver/pb" - "github.com/moby/buildkit/util/entitlements" - "github.com/moby/buildkit/util/grpcerrors" - "github.com/moby/buildkit/util/iohelper" - utilsystem "github.com/moby/buildkit/util/system" - "github.com/moby/buildkit/util/testutil/echoserver" - "github.com/moby/buildkit/util/testutil/integration" - "github.com/moby/buildkit/util/testutil/workers" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "github.com/tonistiigi/fsutil" - "golang.org/x/crypto/ssh/agent" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" -) - -func TestClientGatewayIntegration(t *testing.T) { - integration.Run(t, integration.TestFuncs( - testClientGatewaySolve, - testClientGatewayFailedSolve, - testClientGatewayEmptySolve, - testNoBuildID, - testUnknownBuildID, - testClientGatewayContainerExecPipe, - testClientGatewayContainerExecPipeRelease, - testClientGatewayContainerExecPipeSignalKill, - testClientGatewayContainerCancelOnRelease, - testClientGatewayContainerPID1Fail, - testClientGatewayContainerPID1Exit, - testClientGatewayContainerMounts, - testClientGatewayContainerSecretEnv, - testClientGatewayContainerPID1Tty, - testClientGatewayContainerCancelPID1Tty, - testClientGatewayContainerExecTty, - testClientGatewayContainerCancelExecTty, - testClientSlowCacheRootfsRef, - testClientGatewayContainerPlatformPATH, - testClientGatewayContainerInvalidSecurityMode, - testClientGatewayExecError, - testClientGatewaySlowCacheExecError, - testClientGatewayExecFileActionError, - testClientGatewayContainerExtraHosts, - testClientGatewayContainerSignal, - testWarnings, - testClientGatewayCanceledCredentialsCallbackReturns, - testClientGatewayNilResult, - testClientGatewayEmptyImageExec, - ), integration.WithMirroredImages(integration.OfficialImages("busybox:latest"))) - - integration.Run(t, integration.TestFuncs( - testClientGatewayContainerSecurityModeCaps, - testClientGatewayContainerSecurityModeValidation, - ), integration.WithMirroredImages(integration.OfficialImages("busybox:latest")), - integration.WithMatrix("secmode", map[string]any{ - "sandbox": securitySandbox, - "insecure": securityInsecure, - }), - ) - - integration.Run(t, integration.TestFuncs( - testClientGatewayContainerHostNetworkingAccess, - testClientGatewayContainerHostNetworkingValidation, - ), - integration.WithMirroredImages(integration.OfficialImages("busybox:latest")), - integration.WithMatrix("netmode", map[string]any{ - "default": defaultNetwork, - "host": hostNetwork, - }), - ) -} - -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 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) -} - -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 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 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)) -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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) -} - -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) -} - -// 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) -} - -// 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) -} - -type testPrompt struct { - ctx context.Context - t *testing.T - output *bytes.Buffer - input io.Writer - prompt string - pos int -} - -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: "% ", - } -} - -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): - } - } -} - -// 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) -} - -// 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) -} - -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) -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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) -} - -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) -} - -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 testClientGatewayContainerHostNetworkingAccess(t *testing.T, sb integration.Sandbox) { - testClientGatewayContainerHostNetworking(t, sb, false) -} - -func testClientGatewayContainerHostNetworkingValidation(t *testing.T, sb integration.Sandbox) { - testClientGatewayContainerHostNetworking(t, 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) -} - -// 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) -} - -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 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 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) -} - -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 - } -} diff --git a/client/client_cache_test.go b/client/client_cache_test.go new file mode 100644 index 000000000..03a4d65ca --- /dev/null +++ b/client/client_cache_test.go @@ -0,0 +1,1804 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "strconv" + "testing" + "time" + + ctd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/pkg/namespaces" + cerrdefs "github.com/containerd/errdefs" + cacheimporttypes "github.com/moby/buildkit/cache/remotecache/v1/types" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/exporter/containerimage/exptypes" + "github.com/moby/buildkit/identity" + "github.com/moby/buildkit/util/testutil" + "github.com/moby/buildkit/util/testutil/helpers" + "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" + "github.com/tonistiigi/fsutil" +) + +func testBasicAzblobCacheImportExport(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendAzblob, + ) + + opts := helpers.AzuriteOpts{ + AccountName: "azblobcacheaccount", + AccountKey: base64.StdEncoding.EncodeToString([]byte("azblobcacheaccountkey")), + } + + azAddr, cleanup, err := helpers.NewAzuriteServer(t, sb, opts) + require.NoError(t, err) + defer cleanup() + + im := CacheOptionsEntry{ + Type: "azblob", + Attrs: map[string]string{ + "account_url": azAddr, + "account_name": opts.AccountName, + "secret_access_key": opts.AccountKey, + "container": "cachecontainer", + }, + } + ex := CacheOptionsEntry{ + Type: "azblob", + Attrs: map[string]string{ + "account_url": azAddr, + "account_name": opts.AccountName, + "secret_access_key": opts.AccountKey, + "container": "cachecontainer", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testBasicCacheImportExport(t *testing.T, sb integration.Sandbox, cacheOptionsEntryImport, cacheOptionsEntryExport []CacheOptionsEntry) { + 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) { + baseState := integration.UnixOrWindows(busybox, st) + st = baseState.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + } + + run(integration.UnixOrWindows( + `sh -c "echo -n foobar > const"`, + `cmd /C echo foobar > const`, + )) + run(integration.UnixOrWindows( + `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, + `cmd /C echo %RANDOM% > 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, + }, + }, + CacheExports: cacheOptionsEntryExport, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "const")) + require.NoError(t, err) + newLine := integration.UnixOrWindows("", " \r\n") + require.Equal(t, "foobar"+newLine, string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "unique")) + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + + destDir = t.TempDir() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + CacheImports: cacheOptionsEntryImport, + }, nil) + require.NoError(t, err) + + dt2, err := os.ReadFile(filepath.Join(destDir, "const")) + require.NoError(t, err) + require.Equal(t, "foobar"+newLine, string(dt2)) + + dt2, err = os.ReadFile(filepath.Join(destDir, "unique")) + require.NoError(t, err) + require.Equal(t, string(dt), string(dt2)) +} + +func testBasicInlineCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureDirectPush, + workers.FeatureCacheExport, + workers.FeatureCacheBackendInline, + ) + 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() + + 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) + } + + run(integration.UnixOrWindows( + `sh -c "echo -n foobar > const"`, + `cmd /C echo foobar > const`, + )) + run(integration.UnixOrWindows( + `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, + `cmd /C echo %RANDOM% > unique`, + )) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + target := registry + "/buildkit/testexportinline:latest" + + resp, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + }, nil) + require.NoError(t, err) + + dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureCacheBackendRegistry) + + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + // specifying inline cache exporter is needed for reproducing containerimage.digest + // (not needed for reproducing rootfs/unique) + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + require.Equal(t, dgst, dgst2) + + ensurePruneAll(t, c, sb) + + // Export the cache again with compression + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + // specifying inline cache exporter is needed for reproducing containerimage.digest + // (not needed for reproducing rootfs/unique) + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "compression": "uncompressed", // inline cache should work with compression + "force-compression": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst2uncompress, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + // dgst2uncompress != dgst, because the compression type is different + unique2uncompress, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2uncompress, "/unique") + require.NoError(t, err) + require.Equal(t, unique, unique2uncompress) + + ensurePruneAll(t, c, sb) + + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst3, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + // dgst3 != dgst, because inline cache is not exported for dgst3 + unique3, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst3, "/unique") + require.NoError(t, err) + require.Equal(t, unique, unique3) +} + +func testBasicLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendLocal, + ) + dir := t.TempDir() + im := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "src": dir, + }, + } + ex := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "dest": dir, + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testBasicRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + o := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o}, []CacheOptionsEntry{o}) +} + +func testBasicS3CacheImportExport(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendS3, + ) + + opts := helpers.MinioOpts{ + Region: "us-east-1", + AccessKeyID: "minioadmin", + SecretAccessKey: "minioadmin", + } + + s3Addr, s3Bucket, cleanup, err := helpers.NewMinioServer(t, sb, opts) + require.NoError(t, err) + defer cleanup() + + im := CacheOptionsEntry{ + Type: "s3", + Attrs: map[string]string{ + "region": opts.Region, + "access_key_id": opts.AccessKeyID, + "secret_access_key": opts.SecretAccessKey, + "bucket": s3Bucket, + "endpoint_url": s3Addr, + "use_path_style": "true", + }, + } + ex := CacheOptionsEntry{ + Type: "s3", + Attrs: map[string]string{ + "region": opts.Region, + "access_key_id": opts.AccessKeyID, + "secret_access_key": opts.SecretAccessKey, + "bucket": s3Bucket, + "endpoint_url": s3Addr, + "use_path_style": "true", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testCacheExportCacheDeletedContent(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + tmpdir := integration.Tmpdir(t) + + err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600) + require.NoError(t, err) + + base := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo abc-def > /foo && echo abc > /detection-file"`)).Root() + st := llb.Scratch().File(llb.Copy(base, "/foo", "/out")) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": filepath.Join(tmpdir.Name, "cache"), + "mode": "max", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(tmpdir.Name, "cache", "index.json")) + require.NoError(t, err) + + var index ocispecs.Index + err = json.Unmarshal(dt, &index) + require.NoError(t, err) + + require.Len(t, index.Manifests, 1) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", index.Manifests[0].Digest.Hex())) + require.NoError(t, err) + + var mfst ocispecs.Manifest + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + + require.Len(t, mfst.Layers, 3) + require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst.Config.MediaType) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", mfst.Config.Digest.Hex())) + require.NoError(t, err) + + var cc cacheimporttypes.CacheConfig + err = json.Unmarshal(dt, &cc) + require.NoError(t, err) + + require.Equal(t, 3, len(cc.Layers)) + require.Equal(t, 5, len(cc.Records)) + + var runLayer *int + for i, l := range cc.Layers { + if l.ParentIndex != -1 { + if runLayer != nil { + t.Fatal("multiple RUN layers") + } + runLayer = &i + } + } + require.NotNil(t, runLayer) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex())) + require.NoError(t, err) + + m, err := testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + require.Equal(t, "abc\n", string(m["detection-file"].Data)) + + // delete the blob for the run layer + err = os.Remove(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex())) + require.NoError(t, err) + + // prune all buildkit state + ensurePruneAll(t, c, sb) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + CacheImports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "src": filepath.Join(tmpdir.Name, "cache"), + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": filepath.Join(tmpdir.Name, "cache2"), + "mode": "max", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2", "index.json")) + require.NoError(t, err) + + var index2 ocispecs.Index + err = json.Unmarshal(dt, &index2) + require.NoError(t, err) + + require.Len(t, index2.Manifests, 1) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", index2.Manifests[0].Digest.Hex())) + require.NoError(t, err) + + var mfst2 ocispecs.Manifest + err = json.Unmarshal(dt, &mfst2) + require.NoError(t, err) + + require.Len(t, mfst2.Layers, 1) + require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst2.Config.MediaType) + + dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", mfst2.Config.Digest.Hex())) + require.NoError(t, err) + + var cc2 cacheimporttypes.CacheConfig + err = json.Unmarshal(dt, &cc2) + require.NoError(t, err) + + require.Equal(t, 1, len(cc2.Layers)) + require.Equal(t, 5, len(cc2.Records)) + + for i, r := range cc.Records { + require.Equal(t, cc2.Records[i].Digest, r.Digest) + require.Equal(t, cc2.Records[i].Inputs, r.Inputs) + } +} + +// moby/buildkit#1336 +func testCacheExportCacheKeyLoop(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + tmpdir := integration.Tmpdir(t) + + err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600) + require.NoError(t, err) + + imgName := integration.UnixOrWindows("alpine:latest", "nanoserver:latest") + scratch := integration.UnixOrWindows( + llb.Scratch(), + llb.Image(imgName), + ) + cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0") + for _, mode := range []bool{false, true} { + func(mode bool) { + t.Run(fmt.Sprintf("mode=%v", mode), func(t *testing.T) { + buildbase := llb.Image(imgName).File(llb.Copy(llb.Local("mylocal"), "foo", "foo")) + if mode { // same cache keys with a separating node go to different code-path + buildbase = buildbase.Run(llb.Shlex(cmdStr)).Root() + } + intermed := llb.Image(imgName).File(llb.Copy(buildbase, "foo", "foo")) + final := scratch.File(llb.Copy(intermed, "foo", "foooooo")) + + def, err := final.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": filepath.Join(tmpdir.Name, "cache"), + }, + }, + }, + LocalMounts: map[string]fsutil.FS{ + "mylocal": tmpdir, + }, + }, nil) + require.NoError(t, err) + }) + }(mode) + } +} + +func testCacheExportIgnoreError(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport) + 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) + cmd := integration.UnixOrWindows( + `sh -e -c "echo -n ignore-error > data"`, + "cmd /C echo ignore-error> data", + ) + + st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + tests := map[string]struct { + Exports []ExportEntry + CacheExports []CacheOptionsEntry + expectedErrors []string + }{ + "local-ignore-error": { + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: t.TempDir(), + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": "éèç", + }, + }, + }, + expectedErrors: []string{"failed to solve", "contains value with non-printable ASCII characters"}, + }, + "registry-ignore-error": { + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": "test-registry-ignore-error", + "push": "false", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": "fake-url:5000/myrepo:buildcache", + }, + }, + }, + expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"}, + }, + "s3-ignore-error": { + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: t.TempDir(), + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "s3", + Attrs: map[string]string{ + "endpoint_url": "http://fake-url:9000", + "bucket": "my-bucket", + "region": "us-east-1", + "access_key_id": "minioadmin", + "secret_access_key": "minioadmin", + "use_path_style": "true", + }, + }, + }, + expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"}, + }, + } + ignoreErrorValues := []bool{true, false} + for _, ignoreError := range ignoreErrorValues { + ignoreErrStr := strconv.FormatBool(ignoreError) + for n, test := range tests { + require.Equal(t, 1, len(test.Exports)) + require.Equal(t, 1, len(test.CacheExports)) + require.NotEmpty(t, test.CacheExports[0].Attrs) + test.CacheExports[0].Attrs["ignore-error"] = ignoreErrStr + testName := fmt.Sprintf("%s-%s", n, ignoreErrStr) + t.Run(testName, func(t *testing.T) { + switch n { + case "local-ignore-error": + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendLocal) + case "registry-ignore-error": + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendRegistry) + case "s3-ignore-error": + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendS3) + } + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: test.Exports, + CacheExports: test.CacheExports, + }, nil) + if ignoreError { + require.NoError(t, err) + } else { + require.Error(t, err) + for _, errStr := range test.expectedErrors { + require.Contains(t, err.Error(), errStr) + } + } + }) + } + } +} + +func testImageManifestRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + im := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + } + ex := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + "oci-mediatypes": "true", + "mode": "max", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testLocalCacheExportReset(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + tmpdir := integration.Tmpdir(t) + cacheDir := filepath.Join(tmpdir.Name, "cache") + + // Build 1: export cache for a simple build + st1 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build1 > /out"`)).Root() + def1, err := llb.Scratch().File(llb.Copy(st1, "/out", "/out")).Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def1, SolveOpt{ + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": cacheDir, + }, + }, + }, + }, nil) + require.NoError(t, err) + + // Verify blobs were written + blobDir := filepath.Join(cacheDir, "blobs", "sha256") + entries1, err := os.ReadDir(blobDir) + require.NoError(t, err) + require.Greater(t, len(entries1), 0) + + // Build 2: different build, export with reset=true + st2 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build2-different > /out2"`)).Root() + def2, err := llb.Scratch().File(llb.Copy(st2, "/out2", "/out2")).Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def2, SolveOpt{ + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": cacheDir, + "reset": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + // Read the manifest to determine which blobs should be referenced + dt, err := os.ReadFile(filepath.Join(cacheDir, "index.json")) + require.NoError(t, err) + + var index ocispecs.Index + err = json.Unmarshal(dt, &index) + require.NoError(t, err) + require.Len(t, index.Manifests, 1) + + dt, err = os.ReadFile(filepath.Join(blobDir, index.Manifests[0].Digest.Hex())) + require.NoError(t, err) + + var mfst ocispecs.Manifest + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + + // Build the set of expected blobs: manifest + config + all layers + expectedBlobs := map[string]struct{}{ + index.Manifests[0].Digest.Hex(): {}, + mfst.Config.Digest.Hex(): {}, + } + for _, l := range mfst.Layers { + expectedBlobs[l.Digest.Hex()] = struct{}{} + } + + // Verify only referenced blobs remain + entries2, err := os.ReadDir(blobDir) + require.NoError(t, err) + + actualBlobs := map[string]struct{}{} + for _, e := range entries2 { + actualBlobs[e.Name()] = struct{}{} + } + require.Equal(t, expectedBlobs, actualBlobs, "after reset, only blobs referenced by the current manifest should remain") +} + +func testMultipleCacheExports(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureMultiCacheExport) + 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) + + 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) + } + + constCmd := integration.UnixOrWindows( + `sh -c "echo -n foobar > const"`, + `cmd /C echo foobar > const`, + ) + uniqueCmd := integration.UnixOrWindows( + `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, + `cmd /C echo %RANDOM% > unique`, + ) + + run(constCmd) + run(uniqueCmd) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + target := path.Join(registry, "image:test") + target2 := path.Join(registry, "image-copy:test") + cacheRef := path.Join(registry, "cache:test") + cacheOutDir, cacheOutDir2 := t.TempDir(), t.TempDir() + + res, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": cacheOutDir, + }, + }, + { + Type: "local", + Attrs: map[string]string{ + "dest": cacheOutDir2, + }, + }, + { + Type: "registry", + Attrs: map[string]string{ + "ref": cacheRef, + }, + }, + { + Type: "inline", + }, + }, + }, nil) + require.NoError(t, err) + + ensureFile(t, filepath.Join(cacheOutDir, ocispecs.ImageIndexFile)) + ensureFile(t, filepath.Join(cacheOutDir2, ocispecs.ImageIndexFile)) + + dgst := res.ExporterResponse[exptypes.ExporterImageDigestKey] + + uniqueFile, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") + require.NoError(t, err) + + res, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target2, + "push": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + }, nil) + require.NoError(t, err) + + dgst2 := res.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, dgst, dgst2) + + destDir := t.TempDir() + ensurePruneAll(t, c, sb) + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": cacheRef, + }, + }, + }, + }, nil) + require.NoError(t, err) + + ensureFileContents(t, filepath.Join(destDir, "const"), integration.UnixOrWindows("foobar", "foobar \r\n")) + ensureFileContents(t, filepath.Join(destDir, "unique"), string(uniqueFile)) +} + +func testMultipleRecordsWithSameLayersCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + cacheOpts := []CacheOptionsEntry{{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }} + + requiresLinux(t) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + base := llb.Image("busybox:latest") + // layerA and layerB create identical layers with different LLB + layerA := base.Run(llb.Args([]string{ + "sh", "-c", + `echo $(( 1 + 2 )) > /result && touch -d "1970-01-01 00:00:00" /result`, + })).Root() + layerB := base.Run(llb.Args([]string{ + "sh", "-c", + `echo $(( 2 + 1 )) > /result && touch -d "1970-01-01 00:00:00" /result`, + })).Root() + + combined := llb.Merge([]llb.State{layerA, layerB}) + combinedDef, err := combined.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), combinedDef, SolveOpt{ + CacheExports: cacheOpts, + }, nil) + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + + singleDef, err := layerA.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), singleDef, SolveOpt{ + CacheImports: cacheOpts, + }, nil) + require.NoError(t, err) + + // Ensure that even though layerA and layerB were both loaded as possible results + // and only was used, all the cache refs are released + // More context: https://github.com/moby/buildkit/pull/3815 + ensurePruneAll(t, c, sb) +} + +func testMultipleRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + o := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + } + o2 := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target + "notexist", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o, o2}, []CacheOptionsEntry{o}) +} + +func testRegistryEmptyCacheExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheBackendRegistry, + ) + + for _, ociMediaTypes := range []bool{true, false} { + for _, imageManifest := range []bool{true, false} { + if imageManifest && !ociMediaTypes { + // invalid configuration for remote cache + continue + } + + t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t/imageManifest=%t", ociMediaTypes, imageManifest), func(t *testing.T) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + st := llb.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) + + cacheTarget := registry + "/buildkit/testregistryemptycache:latest" + + cacheOptionsEntry := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": cacheTarget, + "image-manifest": strconv.FormatBool(imageManifest), + "oci-mediatypes": strconv.FormatBool(ociMediaTypes), + }, + } + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + CacheExports: []CacheOptionsEntry{cacheOptionsEntry}, + }, nil) + require.NoError(t, err) + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + cdAddress := sb.ContainerdAddress() + var client *ctd.Client + if cdAddress != "" { + client, err = newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + + _, err := client.Fetch(ctx, cacheTarget) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) + } + }) + } + } +} + +// testSnapshotWithMultipleBlobs verifies that BuildKit handles a single snapshot with multiple +// compressed blob representations. It pushes the same layer with compression levels 0 and 9, +// producing different blob digests, then pulls and exports both images to ensure no conflicts. +// moby/buildkit#3809 +func testSnapshotWithMultipleBlobs(t *testing.T, sb integration.Sandbox) { + // TODO: Re-enable on Windows once the HCS layer activation race is fixed upstream. + // Passes locally but fails on GitHub Actions with: mkdir ...\ProgramData\Microsoft: + // Cannot create a file when that file already exists. The bug is in the Windows + // snapshotter/differ layer activation path, not in this test. + integration.SkipOnPlatform(t, "windows", "HCS layer activation race: concurrent mkdir fails on CI (passes locally)") + workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + // build two images with same layer but different compressed blobs + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + now := time.Now() + + imgName := integration.UnixOrWindows("alpine", "nanoserver") + // On Windows: + // - llb.Scratch() doesn't work (Windows snapshotter requires a base OS layer) + // - llb.Copy across images doesn't work (dual mount: "number of mounts should always be 1") + // So we use llb.Image(nanoserver).File(llb.Mkfile(...)) which only needs one mount. + // The nanoserver base layers appear before our layer, so the final assertion + // compares a layer index that skips the base layers. + st := integration.UnixOrWindows( + llb.Scratch().File(llb.Copy(llb.Image(imgName), "/", "/"+imgName+"/", llb.WithCreatedTime(now))), + llb.Image(imgName).File(llb.Mkfile("/testdata", 0600, []byte("snapshot-test-content"), llb.WithCreatedTime(now))), + ) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + name1 := registry + "/multiblobtest1/image:latest" + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name1, + "push": "true", + "compression-level": "0", + }, + }, + }, + }, nil) + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + + st = st.File(llb.Mkfile("test", 0600, []byte("test"))) // extra layer so we don't get a cache match based on image config rootfs only + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + name2 := registry + "/multiblobtest2/image:latest" + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name2, + "push": "true", + "compression-level": "9", + }, + }, + }, + }, nil) + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + + // first build with first image + destDir := t.TempDir() + + out1 := filepath.Join(destDir, "out1.tar") + outW1, err := os.Create(out1) + require.NoError(t, err) + + st = llb.Image(name1).File(llb.Mkfile("test", 0600, []byte("test1"))) + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterOCI, // make sure to export so blobs need to be loaded + Output: fixedWriteCloser(outW1), + }, + }, + }, nil) + require.NoError(t, err) + + // make sure second image does not cause any errors + out2 := filepath.Join(destDir, "out2.tar") + outW2, err := os.Create(out2) + require.NoError(t, err) + + st = llb.Image(name2).File(llb.Mkfile("test", 0600, []byte("test2"))) + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: integration.UnixOrWindows( + map[string]string{"attest:sbom": ""}, + map[string]string{}, + ), + Exports: []ExportEntry{ + { + Type: ExporterOCI, // make sure to export so blobs need to be loaded + Output: fixedWriteCloser(outW2), + }, + }, + }, nil) + require.NoError(t, err) + + // extra validation that we did get different layer blobs + dt, err := os.ReadFile(out1) + 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) + + dt, err = os.ReadFile(out2) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index) + require.NoError(t, err) + + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &index) + require.NoError(t, err) + + var mfst2 ocispecs.Manifest + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst2) + require.NoError(t, err) + + // On Linux, Layers[0] is the layer we created (copy from alpine). + // On Windows, the nanoserver base layer(s) come first, so our layer is at a later index. + // We find the first layer that differs between the two manifests — that's the layer + // pushed with different compression levels. + layerIdx := integration.UnixOrWindows(0, len(mfst.Layers)-2) + require.NotEqual(t, mfst.Layers[layerIdx].Digest, mfst2.Layers[layerIdx].Digest) +} + +func testUncompressedLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendLocal, + ) + dir := t.TempDir() + im := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "src": dir, + }, + } + ex := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "dest": dir, + "compression": "uncompressed", + "force-compression": "true", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testUncompressedRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + im := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + } + ex := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + "compression": "uncompressed", + "force-compression": "true", + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +// TODO: Re-enable the skipped test on Windows (see issue #6296) +func testZstdLocalCacheExport(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.FeatureCacheExport, workers.FeatureCacheBackendLocal) + 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 -e -c "echo -n zstd > data"`, + `cmd /C "echo zstd > C:\data"`, + ) + + st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) + st = llb.Image(imgName).Run(llb.Shlex(cmdStr), llb.Dir("/wd")).AddMount("/wd", st) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + destDir := t.TempDir() + destOutDir := t.TempDir() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destOutDir, + }, + }, + // compression option should work even with inline cache exports + CacheExports: []CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{ + "dest": destDir, + "image-manifest": "false", + "compression": "zstd", + }, + }, + }, + }, nil) + require.NoError(t, err) + + var index ocispecs.Index + dt, err := os.ReadFile(filepath.Join(destDir, ocispecs.ImageIndexFile)) + require.NoError(t, err) + err = json.Unmarshal(dt, &index) + require.NoError(t, err) + + var layerIndex ocispecs.Index + dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex())) + require.NoError(t, err) + err = json.Unmarshal(dt, &layerIndex) + require.NoError(t, err) + + lastLayer := layerIndex.Manifests[len(layerIndex.Manifests)-2] + require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType) + + zstdLayerDigest := lastLayer.Digest.Hex() + dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest)) + require.NoError(t, err) + require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, dt[:4]) +} + +func testZstdLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendLocal, + ) + dir := t.TempDir() + im := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "src": dir, + }, + } + ex := CacheOptionsEntry{ + Type: "local", + Attrs: map[string]string{ + "dest": dir, + "compression": "zstd", + "force-compression": "true", + "oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype. + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +// TODO: Re-enable the skipped test on Windows (see issue #6296) +func testZstdRegistryCacheImportExport(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.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendRegistry, + ) + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testexport:latest" + im := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + } + ex := CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + "compression": "zstd", + "force-compression": "true", + "image-manifest": "false", + "oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype. + }, + } + testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) +} + +func testStargzLazyInlineCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheImport, + workers.FeatureCacheBackendInline, + workers.FeatureCacheBackendRegistry, + ) + requiresLinux(t) + cdAddress := sb.ContainerdAddress() + if cdAddress == "" || sb.Snapshotter() != "stargz" { + t.Skip("test requires containerd worker with stargz snapshotter") + } + + client, err := newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + var ( + imageService = client.ImageService() + contentStore = client.ContentStore() + ctx = namespaces.WithNamespace(sb.Context(), "buildkit") + ) + + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + // Prepare stargz inline cache + orgImage := "docker.io/library/alpine:latest" + sgzImage := registry + "/stargz/alpine:" + identity.NewID() + baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"})) + def, err := baseDef.Marshal(sb.Context()) + require.NoError(t, err) + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": sgzImage, + "push": "true", + "compression": "estargz", + "oci-mediatypes": "true", + "force-compression": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + }, nil) + require.NoError(t, err) + + // clear all local state out + err = imageService.Delete(ctx, sgzImage, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // stargz layers should be lazy even for executing something on them + def, err = baseDef. + Run(llb.Args([]string{"/bin/touch", "/bar"})). + Marshal(sb.Context()) + require.NoError(t, err) + target := registry + "/buildkit/testlazyimage:" + identity.NewID() + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "oci-mediatypes": "true", + "compression": "estargz", + "store": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzImage, + }, + }, + }, + }, 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) + + // Check if image layers are lazy. + // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. + var sgzLayers []ocispecs.Descriptor + for i, layer := range manifest.Layers[:len(manifest.Layers)-1] { + _, err = contentStore.Info(ctx, layer.Digest) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i) + sgzLayers = append(sgzLayers, layer) + } + require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") + + // The topmost(last) layer created by `Run` shouldn't be lazy + _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) + require.NoError(t, err) + + // clear all local state out + err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // stargz layers should be exportable + 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), + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzImage, + }, + }, + }, + }, nil) + require.NoError(t, err) + + // Check if image layers are un-lazied + for _, layer := range sgzLayers { + _, err = contentStore.Info(ctx, layer.Digest) + require.NoError(t, err) + } + + ensurePruneAll(t, c, sb) +} + +func testStargzLazyRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, + workers.FeatureCacheExport, + workers.FeatureCacheBackendRegistry, + workers.FeatureOCIExporter, + ) + requiresLinux(t) + cdAddress := sb.ContainerdAddress() + if cdAddress == "" || sb.Snapshotter() != "stargz" { + t.Skip("test requires containerd worker with stargz snapshotter") + } + + client, err := newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + var ( + imageService = client.ImageService() + contentStore = client.ContentStore() + ctx = namespaces.WithNamespace(sb.Context(), "buildkit") + ) + + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir := t.TempDir() + + // Prepare stargz registry cache + orgImage := "docker.io/library/alpine:latest" + sgzCache := registry + "/stargz/alpinecache:" + identity.NewID() + baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"})) + def, err := baseDef.Marshal(sb.Context()) + require.NoError(t, err) + 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), + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzCache, + "compression": "estargz", + "oci-mediatypes": "true", + "force-compression": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + // clear all local state out + ensurePruneAll(t, c, sb) + workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureDirectPush) + + // stargz layers should be lazy even for executing something on them + def, err = baseDef. + Run(llb.Args([]string{"sh", "-c", "cat /dev/urandom | head -c 100 | sha256sum > unique"})). + Marshal(sb.Context()) + require.NoError(t, err) + target := registry + "/buildkit/testlazyimage:" + identity.NewID() + resp, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "store": "true", + "oci-mediatypes": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzCache, + }, + }, + }, + CacheExports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzCache, + "compression": "estargz", + "oci-mediatypes": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") + 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) + + // Check if image layers are lazy. + // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. + var sgzLayers []ocispecs.Descriptor + for i, layer := range manifest.Layers[:len(manifest.Layers)-1] { + _, err = contentStore.Info(ctx, layer.Digest) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i) + sgzLayers = append(sgzLayers, layer) + } + require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") + + // The topmost(last) layer created by `Run` shouldn't be lazy + _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) + require.NoError(t, err) + + // Run build again and check if cache is reused + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "store": "true", + "oci-mediatypes": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzCache, + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + unique2, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2, "/unique") + require.NoError(t, err) + + require.Equal(t, dgst, dgst2) + require.Equal(t, unique, unique2) + + // clear all local state out + err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // stargz layers should be exportable + out = filepath.Join(destDir, "out2.tar") + outW, err = os.Create(out) + require.NoError(t, err) + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Output: fixedWriteCloser(outW), + }, + }, + CacheImports: []CacheOptionsEntry{ + { + Type: "registry", + Attrs: map[string]string{ + "ref": sgzCache, + }, + }, + }, + }, nil) + require.NoError(t, err) + + // Check if image layers are un-lazied + for _, layer := range sgzLayers { + _, err = contentStore.Info(ctx, layer.Digest) + require.NoError(t, err) + } + + ensurePruneAll(t, c, sb) +} diff --git a/client/client_control_test.go b/client/client_control_test.go new file mode 100644 index 000000000..693e27a34 --- /dev/null +++ b/client/client_control_test.go @@ -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 + } + } +} diff --git a/client/client_diskusage_test.go b/client/client_diskusage_test.go new file mode 100644 index 000000000..563e30bb8 --- /dev/null +++ b/client/client_diskusage_test.go @@ -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) +} diff --git a/client/client_exec_test.go b/client/client_exec_test.go new file mode 100644 index 000000000..fde26a365 --- /dev/null +++ b/client/client_exec_test.go @@ -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{} +) diff --git a/client/client_export_image_test.go b/client/client_export_image_test.go new file mode 100644 index 000000000..902e1dfae --- /dev/null +++ b/client/client_export_image_test.go @@ -0,0 +1,2005 @@ +package client + +import ( + "archive/tar" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "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/images" + "github.com/containerd/containerd/v2/core/remotes/docker" + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/containerd/v2/plugins/content/local" + cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/platforms" + controlapi "github.com/moby/buildkit/api/services/control" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" + "github.com/moby/buildkit/exporter/containerimage/exptypes" + gateway "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/identity" + "github.com/moby/buildkit/solver/pb" + "github.com/moby/buildkit/util/contentutil" + "github.com/moby/buildkit/util/testutil" + "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" + "golang.org/x/sync/errgroup" +) + +func testBuildExportScratch(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) + 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) + + makeFrontend := func(ps []string) func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + return func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + st := llb.Scratch() + def, err := st.Marshal(sb.Context()) + require.NoError(t, 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() + if ps == nil { + res.SetRef(ref) + } else { + for _, p := range ps { + res.AddRef(p, ref) + } + + expPlatforms := &exptypes.Platforms{ + Platforms: make([]exptypes.Platform, len(ps)), + } + for i, pk := range ps { + p := platforms.MustParse(pk) + + img := ocispecs.Image{ + Platform: p, + Config: ocispecs.ImageConfig{ + Labels: map[string]string{ + "foo": "i am platform " + platforms.Format(p), + }, + }, + } + config, err := json.Marshal(img) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal image config") + } + res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, pk), config) + + expPlatforms.Platforms[i] = exptypes.Platform{ + ID: pk, + Platform: p, + } + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + } + + return res, nil + } + } + + target := registry + "/buildkit/build/exporter:withnocompressed" + + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "unpack": "true", + "compression": "uncompressed", + "attest:provenance": "mode=max", + }, + }, + }, + }, "", makeFrontend(nil), nil) + require.NoError(t, err) + + targetMulti := registry + "/buildkit/build/exporter-multi:withnocompressed" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": targetMulti, + "push": "true", + "unpack": "true", + "compression": "uncompressed", + "attest:provenance": "mode=max", + }, + }, + }, + }, "", makeFrontend([]string{"linux/amd64", "linux/arm64"}), nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Len(t, imgs.Images, 1) + img := imgs.Images[0] + require.Empty(t, img.Layers) + require.True(t, platforms.Only(platforms.DefaultSpec()).Match(img.Img.Platform)) + + desc, provider, err = contentutil.ProviderFromRef(targetMulti) + require.NoError(t, err) + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Len(t, imgs.Images, 2) + img = imgs.Find("linux/amd64") + require.Empty(t, img.Layers) + require.Equal(t, "linux/amd64", platforms.Format(img.Img.Platform)) + require.Equal(t, "i am platform linux/amd64", img.Img.Config.Labels["foo"]) + img = imgs.Find("linux/arm64") + require.Empty(t, img.Layers) + require.Equal(t, "linux/arm64", platforms.Format(img.Img.Platform)) + require.Equal(t, "i am platform linux/arm64", img.Img.Config.Labels["foo"]) +} + +// testBuildExportWithForeignLayer verifies foreign (non-distributable) layer handling during +// image export. propagate=1 preserves foreign media type and URLs; propagate=0 converts to +// regular layers. Skipped on Windows: the test image is Linux-only and BuildKit's Windows +// exporter does not preserve foreign layer media types. +func testBuildExportWithForeignLayer(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows", "test image is Linux-only and BuildKit's Windows exporter strips foreign layer media types") + workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + st := llb.Image("cpuguy83/buildkit-foreign:latest") + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + t.Run("propagate=1", func(t *testing.T) { + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + target := registry + "/buildkit/build/exporter/foreign:latest" + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "oci-mediatypes": "false", + "prefer-nondist-layers": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + + resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true}) + name, desc, err := resolver.Resolve(ctx, target) + require.NoError(t, err) + + fetcher, err := resolver.Fetcher(ctx, name) + require.NoError(t, err) + mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any()) + require.NoError(t, err) + + require.Equal(t, 2, len(mfst.Layers)) + require.Equal(t, images.MediaTypeDockerSchema2LayerForeign, mfst.Layers[0].MediaType) + require.Len(t, mfst.Layers[0].URLs, 1) + require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType) + + rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size}) + require.NoError(t, err) + defer rc.Close() + + // `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request. + // The request is only made when we attempt to read from the reader. + buf := make([]byte, 1) + _, err = rc.Read(buf) + require.Truef(t, cerrdefs.IsNotFound(err), "expected error for blob that should not be in registry: %s, %v", mfst.Layers[0].Digest, err) + }) + t.Run("propagate=0", func(t *testing.T) { + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/build/exporter/noforeign:latest" + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "oci-mediatypes": "false", + }, + }, + }, + }, nil) + require.NoError(t, err) + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + + resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true}) + name, desc, err := resolver.Resolve(ctx, target) + require.NoError(t, err) + + fetcher, err := resolver.Fetcher(ctx, name) + require.NoError(t, err) + + mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any()) + require.NoError(t, err) + + require.Equal(t, 2, len(mfst.Layers)) + require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[0].MediaType) + require.Len(t, mfst.Layers[0].URLs, 0) + require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType) + + rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size}) + require.NoError(t, err) + defer rc.Close() + + // `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request. + // The request is only made when we attempt to read from the reader. + buf := make([]byte, 1) + _, err = rc.Read(buf) + require.NoError(t, err) + }) +} + +func testBuildExportWithUncompressed(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) + requiresLinux(t) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + busybox := llb.Image("busybox:latest") + cmd := `sh -e -c "echo -n uncompressed > data"` + + st := llb.Scratch() + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + + 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/build/exporter:withnocompressed" + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "compression": "uncompressed", + }, + }, + }, + }, nil) + require.NoError(t, err) + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + cdAddress := sb.ContainerdAddress() + var client *ctd.Client + if cdAddress != "" { + client, err = newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + + img, err := client.GetImage(ctx, target) + require.NoError(t, err) + mfst, err := images.Manifest(ctx, client.ContentStore(), img.Target(), nil) + require.NoError(t, err) + require.Equal(t, 1, len(mfst.Layers)) + require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType) + } + + // new layer with gzip compression + targetImg := llb.Image(target) + cmd = `sh -e -c "echo -n gzip > data"` + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", targetImg) + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + compressedTarget := registry + "/buildkit/build/exporter:withcompressed" + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": compressedTarget, + "push": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + allCompressedTarget := registry + "/buildkit/build/exporter:withallcompressed" + _, err = c.Solve(context.TODO(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": allCompressedTarget, + "push": "true", + "compression": "gzip", + "force-compression": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + if cdAddress == "" { + t.Skip("rest of test requires containerd worker") + } + + err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) + require.NoError(t, err) + err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete()) + require.NoError(t, err) + err = client.ImageService().Delete(ctx, allCompressedTarget, images.SynchronousDelete()) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) + + // check if the new layer is compressed with compression option + img, err := client.Pull(ctx, compressedTarget) + require.NoError(t, err) + + dt, err := content.ReadBlob(ctx, img.ContentStore(), img.Target()) + require.NoError(t, err) + + mfst := struct { + MediaType string `json:"mediaType,omitempty"` + ocispecs.Manifest + }{} + + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + require.Equal(t, 2, len(mfst.Layers)) + require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType) + require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) + require.NoError(t, err) + + m, err := testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + item, ok := m["data"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("uncompressed"), item.Data) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest}) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + item, ok = m["data"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("gzip"), item.Data) + + err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete()) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) + + // check if all layers are compressed with force-compressoin option + img, err = client.Pull(ctx, allCompressedTarget) + require.NoError(t, err) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target()) + require.NoError(t, err) + + mfst = struct { + MediaType string `json:"mediaType,omitempty"` + ocispecs.Manifest + }{} + + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + require.Equal(t, 2, len(mfst.Layers)) + require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType) + require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + item, ok = m["data"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("uncompressed"), item.Data) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest}) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + item, ok = m["data"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("gzip"), item.Data) +} + +// testBuildExportZstd verifies OCI export with Zstd-compressed layers. +// It builds a simple layer, exports it with Zstd compression, and validates both +// OCI and Docker schema 2 media types. +func testBuildExportZstd(t *testing.T, sb integration.Sandbox) { + // Skipped on Windows: + // 1. AddMount() fails because Windows snapshots allow only a single mount per layer. + // 2. OCI export fails because windowsLcowDiff lacks the Compare method needed to + // generate compressed layer diffs. + integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', OCI export fails with 'windowsLcowDiff does not implement Compare method'") + workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + busybox := llb.Image("busybox:latest") + cmd := `sh -e -c "echo -n zstd > data"` + + st := llb.Scratch() + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + + 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), + Attrs: map[string]string{ + "compression": "zstd", + }, + }, + }, + // compression option should work even with inline cache exports + CacheExports: []CacheOptionsEntry{ + { + Type: "inline", + }, + }, + }, 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] + require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType) + + zstdLayerDigest := lastLayer.Digest.Hex() + require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4]) + + // repeat without oci mediatype + outW, err = os.Create(out) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Output: fixedWriteCloser(outW), + Attrs: map[string]string{ + "compression": "zstd", + "oci-mediatypes": "false", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dt, err = os.ReadFile(out) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index) + require.NoError(t, err) + + 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] + require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, lastLayer.MediaType) + + require.Equal(t, lastLayer.Digest.Hex(), zstdLayerDigest) +} + +func testBuildPushAndValidate(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() + + 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 foo/sub; echo -n first > foo/sub/bar; chmod 0741 foo;"`) + run(`true`) // this doesn't create a layer + run(`sh -c "echo -n second > foo/sub/baz"`) + + 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/testpush: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) + + // test existence of the image with next build + firstBuild := llb.Image(target) + + def, err = firstBuild.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, "foo/sub/bar")) + require.NoError(t, err) + require.Equal(t, dt, []byte("first")) + + dt, err = os.ReadFile(filepath.Join(destDir, "foo/sub/baz")) + require.NoError(t, err) + require.Equal(t, dt, []byte("second")) + + fi, err := os.Stat(filepath.Join(destDir, "foo")) + require.NoError(t, err) + require.Equal(t, 0741, int(fi.Mode()&0777)) + + checkAllReleasable(t, c, sb, false) + + // examine contents of exported tars (requires containerd) + cdAddress := sb.ContainerdAddress() + if cdAddress == "" { + t.Skip("rest of test requires containerd worker") + } + + // 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() + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + + // check image in containerd + _, err = client.ImageService().Get(ctx, target) + require.NoError(t, err) + + // deleting image should release all content + err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) + + img, err := client.Pull(ctx, target) + require.NoError(t, err) + + desc, err := img.Config(ctx) + require.NoError(t, err) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), desc) + require.NoError(t, err) + + var ociimg ocispecs.Image + err = json.Unmarshal(dt, &ociimg) + require.NoError(t, err) + + require.NotEqual(t, "", ociimg.OS) + require.NotEqual(t, "", ociimg.Architecture) + require.NotEqual(t, "", ociimg.Config.WorkingDir) + require.Equal(t, "layers", ociimg.RootFS.Type) + require.Equal(t, 3, len(ociimg.RootFS.DiffIDs)) + require.NotNil(t, ociimg.Created) + require.Less(t, time.Since(*ociimg.Created), 2*time.Minute) + require.Condition(t, func() bool { + for _, env := range ociimg.Config.Env { + if strings.HasPrefix(env, "PATH=") { + return true + } + } + return false + }) + + require.Equal(t, 3, len(ociimg.History)) + require.Contains(t, ociimg.History[0].CreatedBy, "foo/sub/bar") + require.Contains(t, ociimg.History[1].CreatedBy, "true") + require.Contains(t, ociimg.History[2].CreatedBy, "foo/sub/baz") + require.False(t, ociimg.History[0].EmptyLayer) + require.False(t, ociimg.History[1].EmptyLayer) + require.False(t, ociimg.History[2].EmptyLayer) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target()) + require.NoError(t, err) + + mfst := struct { + MediaType string `json:"mediaType,omitempty"` + ocispecs.Manifest + }{} + + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + + require.Equal(t, ocispecs.MediaTypeImageManifest, mfst.MediaType) + require.Equal(t, 3, len(mfst.Layers)) + require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType) + require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) + require.NoError(t, err) + + m, err := testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + item, ok := m["foo/"] + require.True(t, ok) + require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) + require.Equal(t, 0741, int(item.Header.Mode&0777)) + + item, ok = m["foo/sub/"] + require.True(t, ok) + require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) + + item, ok = m["foo/sub/bar"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("first"), item.Data) + + _, ok = m["foo/sub/baz"] + require.False(t, ok) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[2].Digest}) + require.NoError(t, err) + + m, err = testutil.ReadTarToMap(dt, true) + require.NoError(t, err) + + item, ok = m["foo/sub/baz"] + require.True(t, ok) + require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) + require.Equal(t, []byte("second"), item.Data) + + item, ok = m["foo/"] + require.True(t, ok) + require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) + require.Equal(t, 0741, int(item.Header.Mode&0777)) + + item, ok = m["foo/sub/"] + require.True(t, ok) + require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) + + _, ok = m["foo/sub/bar"] + require.False(t, ok) +} + +// #877 +func testExportBusyboxLocal(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + def, err := llb.Image("busybox").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) + + fi, err := os.Stat(filepath.Join(destDir, "bin/busybox")) + require.NoError(t, err) + + fi2, err := os.Stat(filepath.Join(destDir, "bin/vi")) + require.NoError(t, err) + + require.True(t, os.SameFile(fi, fi2)) +} + +func testExportedImageLabels(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + cdAddress := sb.ContainerdAddress() + if cdAddress == "" { + t.Skip("only supported with containerd") + } + + ctx := sb.Context() + + imgName := integration.UnixOrWindows("busybox", "nanoserver") + prefix := integration.UnixOrWindows( + "", + "cmd /C ", // TODO(profnandaa): currently needs the shell prefix, to be fixed + ) + def, err := llb.Image(imgName). + Run(llb.Shlexf(fmt.Sprintf("%secho foo > /foo", prefix))). + Marshal(ctx) + require.NoError(t, err) + + target := "docker.io/buildkit/build/exporter:labels" + + _, err = c.Solve(ctx, def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + }, + }, + }, + }, nil) + require.NoError(t, err) + + client, err := newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + + ctx = namespaces.WithNamespace(ctx, "buildkit") + + img, err := client.GetImage(ctx, target) + require.NoError(t, err) + + store := client.ContentStore() + + info, err := store.Info(ctx, img.Target().Digest) + require.NoError(t, err) + + dt, err := content.ReadBlob(ctx, store, img.Target()) + require.NoError(t, err) + + var mfst ocispecs.Manifest + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + + require.Equal(t, 2, len(mfst.Layers)) + + hasLabel := func(dgst digest.Digest) bool { + for k, v := range info.Labels { + if strings.HasPrefix(k, "containerd.io/gc.ref.content.") && v == dgst.String() { + return true + } + } + return false + } + + // check that labels are set on all layers and config + for _, l := range mfst.Layers { + require.True(t, hasLabel(l.Digest)) + } + require.True(t, hasLabel(mfst.Config.Digest)) + + err = c.Prune(sb.Context(), nil, PruneAll) + require.NoError(t, err) + + // layer should not be deleted + _, err = store.Info(ctx, mfst.Layers[1].Digest) + require.NoError(t, err) + + err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) + require.NoError(t, err) + + // layers should be deleted + _, err = store.Info(ctx, mfst.Layers[1].Digest) + require.Error(t, err) + require.True(t, errors.Is(err, cerrdefs.ErrNotFound)) + + // config should be deleted + _, err = store.Info(ctx, mfst.Config.Digest) + require.Error(t, err) + require.True(t, errors.Is(err, cerrdefs.ErrNotFound)) + + // buildkit contentstore still has the layer because it is multi-ns + bkstore := proxy.NewContentStore(c.ContentClient()) + + // layer should be deleted as not kept by history + _, err = bkstore.Info(ctx, mfst.Layers[1].Digest) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + + // config should still be there + _, err = bkstore.Info(ctx, img.Metadata().Target.Digest) + require.NoError(t, err) + + _, err = bkstore.Info(ctx, mfst.Config.Digest) + require.NoError(t, err) + + 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) + } + + // now everything should be deleted + _, err = bkstore.Info(ctx, img.Metadata().Target.Digest) + require.Error(t, err) + + _, err = bkstore.Info(ctx, mfst.Config.Digest) + require.Error(t, err) +} + +func testImageResolveConfigDefaultLocalFallback(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) + + cdAddress := sb.ContainerdAddress() + if cdAddress == "" { + t.Skip("test requires containerd worker") + } + + ctx := sb.Context() + c, err := New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + cdClient, err := newContainerd(cdAddress) + require.NoError(t, err) + defer cdClient.Close() + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + target := registry + "/buildkit/default-local-fallback:" + identity.NewID() + st := integration.UnixOrWindows( + llb.Scratch(), + llb.Image("nanoserver:latest"), + ) + def, err := st.File(llb.Mkfile("/fallback", 0600, []byte("local"))).Marshal(ctx) + require.NoError(t, err) + + _, err = c.Solve(ctx, def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "store": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + imageService := cdClient.ImageService() + imageStoreCtx := namespaces.WithNamespace(ctx, "buildkit") + img, err := imageService.Get(imageStoreCtx, target) + require.NoError(t, err) + + _, err = c.Build(ctx, SolveOpt{}, "buildkit_test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + ref, dgst, config, err := c.ResolveImageConfig(ctx, target, sourceresolver.Opt{ + ImageOpt: &sourceresolver.ResolveImageOpt{ + ResolveMode: pb.AttrImageResolveModeDefault, + }, + }) + if err != nil { + return nil, err + } + require.Equal(t, target, ref) + require.Equal(t, img.Target.Digest, dgst) + require.NotEmpty(t, config) + + var ociimg ocispecs.Image + require.NoError(t, json.Unmarshal(config, &ociimg)) + require.NotEmpty(t, ociimg.OS) + require.NotEmpty(t, ociimg.Architecture) + + return gateway.NewResult(), nil + }, nil) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) +} + +func testInvalidExporter(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") + def, err := llb.Image(imgName).Marshal(sb.Context()) + require.NoError(t, err) + + destDir := t.TempDir() + + target := "example.com/buildkit/testoci:latest" + attrs := map[string]string{ + "name": target, + } + for _, exp := range []string{ExporterOCI, ExporterDocker} { + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: exp, + Attrs: attrs, + }, + }, + }, nil) + // output file writer is required + require.Error(t, err) + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: exp, + Attrs: attrs, + OutputDir: destDir, + }, + }, + }, nil) + // output directory is not supported + require.Error(t, err) + } + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + Attrs: attrs, + }, + }, + }, nil) + // output directory is required + require.Error(t, err) + + f, err := os.Create(filepath.Join(destDir, "a")) + require.NoError(t, err) + defer f.Close() + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + Attrs: attrs, + Output: fixedWriteCloser(f), + }, + }, + }, nil) + // output file writer is not supported + require.Error(t, err) + + checkAllReleasable(t, c, sb, true) +} + +func testLazyImagePush(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush) + 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 := integration.UnixOrWindows("busybox:latest", "nanoserver:latest") + def, err := llb.Image(sourceImage).Marshal(sb.Context()) + require.NoError(t, err) + + targetNoTag := registry + "/buildkit/testlazyimage:" + target := targetNoTag + "latest" + _, 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", + }, + }, + }, + }, nil) + require.NoError(t, err) + + imageService := client.ImageService() + contentStore := client.ContentStore() + + img, err := imageService.Get(ctx, target) + require.NoError(t, err) + + manifest, err := images.Manifest(ctx, contentStore, img.Target, nil) + require.NoError(t, err) + + for _, layer := range manifest.Layers { + _, err = contentStore.Info(ctx, layer.Digest) + require.NoError(t, err) + } + + // clear all local state out + err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // retag the image we just pushed with no actual changes, which + // should not result in the image getting un-lazied + def, err = llb.Image(target).Marshal(sb.Context()) + require.NoError(t, err) + + target2 := targetNoTag + "newtag" + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target2, + "push": "true", + "store": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + img, err = imageService.Get(ctx, target2) + require.NoError(t, err) + + manifest, err = images.Manifest(ctx, contentStore, img.Target, nil) + require.NoError(t, err) + + for _, layer := range manifest.Layers { + _, err = contentStore.Info(ctx, layer.Digest) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) + } + + // clear all local state out again + err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // try a cross-repo push to same registry, which should still result in the + // image remaining lazy + target3 := registry + "/buildkit/testlazycrossrepo:latest" + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target3, + "push": "true", + "store": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + img, err = imageService.Get(ctx, target3) + require.NoError(t, err) + + manifest, err = images.Manifest(ctx, contentStore, img.Target, nil) + require.NoError(t, err) + + for _, layer := range manifest.Layers { + _, err = contentStore.Info(ctx, layer.Digest) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) + } + + // check that a subsequent build can use the previously lazy image in an exec + cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0") + def, err = llb.Image(target2).Run(llb.Args([]string{cmdStr})).Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testOCIExporter(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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + target := "example.com/buildkit/testoci:latest" + for _, tc := range []struct { + name string + exporter string + attrs map[string]string + indexMediaType string + manifestMediaType string + configMediaType string + layerMediaType string + }{ + { + name: ExporterOCI, + exporter: ExporterOCI, + attrs: map[string]string{}, + indexMediaType: ocispecs.MediaTypeImageIndex, + manifestMediaType: ocispecs.MediaTypeImageManifest, + configMediaType: ocispecs.MediaTypeImageConfig, + layerMediaType: ocispecs.MediaTypeImageLayerGzip, + }, + { + name: ExporterDocker, + exporter: ExporterDocker, + attrs: map[string]string{ + "name": target, + }, + indexMediaType: ocispecs.MediaTypeImageIndex, + manifestMediaType: ocispecs.MediaTypeImageManifest, + configMediaType: ocispecs.MediaTypeImageConfig, + layerMediaType: ocispecs.MediaTypeImageLayerGzip, + }, + { + name: "docker-oci-mediatypes-false", + exporter: ExporterDocker, + attrs: map[string]string{ + "name": target, + "oci-mediatypes": "false", + }, + indexMediaType: ocispecs.MediaTypeImageIndex, + manifestMediaType: images.MediaTypeDockerSchema2Manifest, + configMediaType: images.MediaTypeDockerSchema2Config, + layerMediaType: images.MediaTypeDockerSchema2LayerGzip, + }, + } { + t.Run(tc.name, func(t *testing.T) { + 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: tc.exporter, + Attrs: tc.attrs, + 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) + + _, 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, tc.indexMediaType, index.MediaType) + require.Equal(t, 1, len(index.Manifests)) + require.Equal(t, tc.manifestMediaType, index.Manifests[0].MediaType) + + var mfst ocispecs.Manifest + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst) + require.NoError(t, err) + require.Equal(t, tc.manifestMediaType, mfst.MediaType) + require.Equal(t, tc.configMediaType, mfst.Config.MediaType) + require.Equal(t, 2, len(mfst.Layers)) + for _, layer := range mfst.Layers { + require.Equal(t, tc.layerMediaType, layer.MediaType) + } + + var ociimg ocispecs.Image + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Config.Digest.Hex()].Data, &ociimg) + require.NoError(t, err) + require.Equal(t, "layers", ociimg.RootFS.Type) + require.Equal(t, 2, len(ociimg.RootFS.DiffIDs)) + + _, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[0].Digest.Hex()] + require.True(t, ok) + _, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[1].Digest.Hex()] + require.True(t, ok) + + if tc.exporter != ExporterDocker { + return + } + + var dockerMfst []struct { + Config string + RepoTags []string + Layers []string + } + err = json.Unmarshal(m["manifest.json"].Data, &dockerMfst) + require.NoError(t, err) + require.Equal(t, 1, len(dockerMfst)) + + _, ok = m[dockerMfst[0].Config] + require.True(t, ok) + require.Equal(t, 2, len(dockerMfst[0].Layers)) + require.Equal(t, 1, len(dockerMfst[0].RepoTags)) + require.Equal(t, target, dockerMfst[0].RepoTags[0]) + + for _, l := range dockerMfst[0].Layers { + _, ok := m[l] + require.True(t, ok) + } + }) + } + + checkAllReleasable(t, c, sb, true) +} + +func testOCIExporterContentStore(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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + for _, exp := range []string{ExporterOCI, ExporterDocker} { + destDir := t.TempDir() + target := "example.com/buildkit/testoci:latest" + + outTar := filepath.Join(destDir, "out.tar") + outW, err := os.Create(outTar) + require.NoError(t, err) + attrs := map[string]string{} + if exp == ExporterDocker { + attrs["name"] = target + } + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: exp, + Attrs: attrs, + Output: fixedWriteCloser(outW), + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(outTar) + require.NoError(t, err) + m, err := testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + checkStore := func(dir string) { + err = filepath.Walk(dir, func(filename string, fi os.FileInfo, err error) error { + filename = strings.TrimPrefix(filename, dir) + filename = strings.Trim(filename, "/") + if filename == "" || filename == "ingest" { + return nil + } + + if fi.IsDir() { + require.Contains(t, m, filename+"/") + } else { + require.Contains(t, m, filename) + if filename == ocispecs.ImageIndexFile { + // this file has a timestamp in it, so we can't compare + return nil + } + f, err := os.Open(path.Join(dir, filename)) + require.NoError(t, err) + data, err := io.ReadAll(f) + require.NoError(t, err) + require.Equal(t, m[filename].Data, data) + } + return nil + }) + require.NoError(t, err) + } + + outDir := filepath.Join(destDir, "out.d") + attrs = map[string]string{ + "tar": "false", + } + if exp == ExporterDocker { + attrs["name"] = target + } + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: exp, + Attrs: attrs, + OutputDir: outDir, + }, + }, + }, nil) + require.NoError(t, err) + checkStore(outDir) + + outStoreDir := filepath.Join(destDir, "store.d") + store, err := local.NewStore(outStoreDir) + require.NoError(t, err) + attrs = map[string]string{ + "tar": "false", + } + if exp == ExporterDocker { + attrs["name"] = target + } + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: exp, + Attrs: attrs, + OutputStore: store, + }, + }, + }, nil) + require.NoError(t, err) + checkStore(outDir) + } + + checkAllReleasable(t, c, sb, true) +} + +func testPullWithDigestCheck(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data1"))) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + name1 := registry + "/foo/bar:v1.0.0" + + resp, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name1, + "push": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst1Str := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + dgst1, err := digest.Parse(dgst1Str) + require.NoError(t, err) + + st = llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data2"))) + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + name2 := registry + "/foo/bar:v2.0.0" + + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name2, + "push": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst2str := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + dgst2, err := digest.Parse(dgst2str) + require.NoError(t, err) + + require.NotEqual(t, dgst1, dgst2) + + // if digest is set in ref then pull happens only by the digest + st = llb.Image(name2 + "@" + dgst1.String()) + 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, "foo")) + require.NoError(t, err) + require.Equal(t, "data1", string(dt)) + + // if digest is set by checksum then pull happens by tag + st = llb.Image(name2, llb.WithImageChecksum(dgst2)) + 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, "foo")) + require.NoError(t, err) + require.Equal(t, "data2", string(dt)) + + // if checksum doesn't match then pull fails + st = llb.Image(name2, llb.WithImageChecksum(dgst1)) + 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(), fmt.Sprintf("image digest %s for %s does not match expected checksum %s", dgst2, name2, dgst1)) +} + +// testPullZstdImage verifies pulling and re-exporting a Zstd-compressed image. +// It pushes a Zstd-compressed image to a registry, pulls it back, and validates +// the layer media types and Zstd magic bytes for both OCI and Docker schema 2. +func testPullZstdImage(t *testing.T, sb integration.Sandbox) { + // Skipped on Windows: + // 1. AddMount() fails because Windows snapshots allow only a single mount per layer. + // 2. Zstd-compressed push/export fails because windowsLcowDiff lacks the Compare + // method needed to generate compressed layer diffs. + integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', Zstd export fails with 'windowsLcowDiff does not implement Compare method'") + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush) + for _, ociMediaTypes := range []bool{true, false} { + t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t", ociMediaTypes), func(t *testing.T) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + busybox := llb.Image("busybox:latest") + cmd := `sh -e -c "echo -n zstd > data"` + + st := llb.Scratch() + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + + 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/build/exporter:zstd" + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "compression": "zstd", + "oci-mediatypes": strconv.FormatBool(ociMediaTypes), + }, + }, + }, + }, nil) + require.NoError(t, err) + + ensurePruneAll(t, c, sb) + + st = llb.Image(target).File(llb.Copy(llb.Image(target), "/data", "/zdata")) + 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), + Attrs: map[string]string{ + "oci-mediatypes": strconv.FormatBool(ociMediaTypes), + }, + }, + }, + }, 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) + + firstLayer := mfst.Layers[0] + if ociMediaTypes { + require.Equal(t, ocispecs.MediaTypeImageLayerZstd, firstLayer.MediaType) + } else { + require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, firstLayer.MediaType) + } + + zstdLayerDigest := firstLayer.Digest.Hex() + require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4]) + }) + } +} + +func testPushByDigest(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data"))) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + name := registry + "/foo/bar" + + resp, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": name, + "push": "true", + "push-by-digest": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + _, _, err = contentutil.ProviderFromRef(name + ":latest") + require.Error(t, err) + + desc, _, err := contentutil.ProviderFromRef(name + "@" + resp.ExporterResponse[exptypes.ExporterImageDigestKey]) + require.NoError(t, err) + + require.Equal(t, resp.ExporterResponse[exptypes.ExporterImageDigestKey], desc.Digest.String()) + require.Equal(t, ocispecs.MediaTypeImageManifest, desc.MediaType) + require.Greater(t, desc.Size, int64(0)) +} + +func testPushProgressSameVertex(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data"))) + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + imageName := registry + "/foo/bar:latest" + + var vertexes []*Vertex + var statuses []*VertexStatus + ch := make(chan *SolveStatus) + eg, ctx := errgroup.WithContext(sb.Context()) + eg.Go(func() error { + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case ss, ok := <-ch: + if !ok { + return nil + } + vertexes = append(vertexes, ss.Vertexes...) + statuses = append(statuses, ss.Statuses...) + } + } + }) + eg.Go(func() error { + _, err := c.Solve(ctx, def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: "image", + Attrs: map[string]string{ + "name": imageName, + "push": "true", + }, + }, + }, + }, ch) + return err + }) + require.NoError(t, eg.Wait()) + + exportVertexes := map[digest.Digest]struct{}{} + pushVertexes := map[digest.Digest]struct{}{} + pushManifestPrefix := "pushing manifest for " + imageName + "@" + + for _, v := range vertexes { + if v.Name == "exporting to image" { + exportVertexes[v.Digest] = struct{}{} + } + } + + for _, st := range statuses { + switch { + case st.ID == "pushing layers": + pushVertexes[st.Vertex] = struct{}{} + case strings.HasPrefix(st.ID, pushManifestPrefix): + pushVertexes[st.Vertex] = struct{}{} + } + } + + require.Len(t, exportVertexes, 1, "expected exactly one export vertex in status stream") + require.NotEmpty(t, pushVertexes, "expected at least one push status in status stream") + + var exportVertex digest.Digest + for v := range exportVertexes { + exportVertex = v + } + for v := range pushVertexes { + require.Equal(t, exportVertex, v, "push statuses should use the same vertex as exporting to image") + } + + hasPushManifest := false + for _, st := range statuses { + if strings.HasPrefix(st.ID, pushManifestPrefix) { + hasPushManifest = true + require.Equal(t, exportVertex, st.Vertex, "pushing manifest should use the same vertex as exporting to image") + } + } + require.True(t, hasPushManifest, "expected pushing manifest status in status stream") +} + +func testStargzLazyPull(t *testing.T, sb integration.Sandbox) { + requiresLinux(t) + + cdAddress := sb.ContainerdAddress() + if cdAddress == "" || sb.Snapshotter() != "stargz" { + t.Skip("test requires containerd worker with stargz snapshotter") + } + + client, err := newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + var ( + imageService = client.ImageService() + contentStore = client.ContentStore() + ctx = namespaces.WithNamespace(sb.Context(), "buildkit") + ) + + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + // Prepare stargz image + orgImage := "docker.io/library/alpine:latest" + sgzImage := registry + "/stargz/alpine:" + identity.NewID() + def, err := llb.Image(orgImage).Marshal(sb.Context()) + require.NoError(t, err) + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": sgzImage, + "push": "true", + "compression": "estargz", + "oci-mediatypes": "true", + "force-compression": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + // clear all local state out + err = imageService.Delete(ctx, sgzImage, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // stargz layers should be lazy even for executing something on them + def, err = llb.Image(sgzImage). + Run(llb.Args([]string{"sh", "-c", "cat /dev/urandom | head -c 100 | sha256sum > unique"})). + Marshal(sb.Context()) + require.NoError(t, err) + target := registry + "/buildkit/testlazyimage:" + identity.NewID() + resp, err := c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "oci-mediatypes": "true", + "store": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") + 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) + + // Check if image layers are lazy. + // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. + var sgzLayers []ocispecs.Descriptor + for _, layer := range manifest.Layers[:len(manifest.Layers)-1] { + _, err = contentStore.Info(ctx, layer.Digest) + require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) + sgzLayers = append(sgzLayers, layer) + } + require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") + + // The topmost(last) layer created by `Run` shouldn't be lazy + _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) + require.NoError(t, err) + + // Run build again and check if cache is reused + resp, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "store": "true", + "oci-mediatypes": "true", + "unsafe-internal-store-allow-incomplete": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] + require.Equal(t, true, ok) + + unique2, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2, "/unique") + require.NoError(t, err) + + require.Equal(t, dgst, dgst2) + require.Equal(t, unique, unique2) + + // clear all local state out + err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + checkAllReleasable(t, c, sb, true) + + // stargz layers should be exportable + 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) + + // Check if image layers are un-lazied + for _, layer := range sgzLayers { + _, err = contentStore.Info(ctx, layer.Digest) + require.NoError(t, err) + } + + ensurePruneAll(t, c, sb) +} diff --git a/client/client_export_local_test.go b/client/client_export_local_test.go new file mode 100644 index 000000000..1ad84d5de --- /dev/null +++ b/client/client_export_local_test.go @@ -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) +} diff --git a/client/client_export_metadata_test.go b/client/client_export_metadata_test.go new file mode 100644 index 000000000..819be230a --- /dev/null +++ b/client/client_export_metadata_test.go @@ -0,0 +1,2422 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + 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/distribution/reference" + intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" + "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/solver/pb" + "github.com/moby/buildkit/solver/result" + "github.com/moby/buildkit/util/attestation" + "github.com/moby/buildkit/util/contentutil" + "github.com/moby/buildkit/util/testutil" + containerdutil "github.com/moby/buildkit/util/testutil/containerd" + "github.com/moby/buildkit/util/testutil/integration" + "github.com/moby/buildkit/util/testutil/workers" + policyimage "github.com/moby/policy-helpers/image" + digest "github.com/opencontainers/go-digest" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/spdx/tools-golang/spdx" + "github.com/stretchr/testify/require" + "golang.org/x/mod/semver" +) + +func testAttestationBundle(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) + + ps := []ocispecs.Platform{ + platforms.MustParse("linux/amd64"), + } + + scratch := func() llb.State { + return integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) + } + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + expPlatforms := &exptypes.Platforms{} + + for _, p := range ps { + pk := platforms.Format(p) + expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) + + // build image + st := scratch().File( + llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), + ) + 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(pk, ref) + + stmt := intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV1, + PredicateType: "https://example.com/attestations/v1.0", + }, + Predicate: map[string]any{ + "foo": "1", + }, + } + buff := bytes.NewBuffer(nil) + enc := json.NewEncoder(buff) + require.NoError(t, enc.Encode(stmt)) + + // build attestations + st = scratch().File( + llb.Mkdir("/bundle", 0700), + ) + st = st.File( + llb.Mkfile("/bundle/attestation.json", 0600, buff.Bytes()), + ) + 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 + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_Bundle, + Ref: refAttest, + Path: "/bundle", + }) + } + + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + target := registry + "/buildkit/testattestationsbundle:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, len(ps)*2, len(imgs.Images)) + + var bases []*testutil.ImageInfo + for _, p := range ps { + pk := platforms.Format(p) + bases = append(bases, imgs.Find(pk)) + } + + atts := imgs.Filter("unknown/unknown") + require.Equal(t, len(ps)*1, len(atts.Images)) + for i, att := range atts.Images { + require.Equal(t, 1, len(att.LayersRaw)) + var attest intoto.Statement + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + + require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) + require.Equal(t, map[string]any{"foo": "1"}, attest.Predicate) + name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsbundle@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i]))) + subjects := []intoto.Subject{{ + Name: name, + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, + }} + require.Equal(t, subjects, attest.Subject) + } +} + +func testAttestationDefaultSubject(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) + + ps := []ocispecs.Platform{ + platforms.MustParse("linux/amd64"), + } + + success := []byte(`{"success": true}`) + scratch := func() llb.State { + return integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) + } + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + expPlatforms := &exptypes.Platforms{} + + for _, p := range ps { + pk := platforms.Format(p) + expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) + + // build image + st := scratch().File( + llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), + ) + 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(pk, ref) + + // build attestations + st = scratch().File(llb.Mkfile("/attestation.json", 0600, success)) + 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 + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_InToto, + Ref: refAttest, + Path: "/attestation.json", + InToto: result.InTotoAttestation{ + PredicateType: "https://example.com/attestations/v1.0", + }, + }) + } + + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + target := registry + "/buildkit/testattestationsemptysubject:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, len(ps)*2, len(imgs.Images)) + + var bases []*testutil.ImageInfo + for _, p := range ps { + pk := platforms.Format(p) + bases = append(bases, imgs.Find(pk)) + } + + atts := imgs.Filter("unknown/unknown") + require.Equal(t, len(ps), len(atts.Images)) + for i, att := range atts.Images { + var attest intoto.Statement + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) + require.Equal(t, map[string]any{"success": true}, attest.Predicate) + + name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsemptysubject@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i]))) + subjects := []intoto.Subject{{ + Name: name, + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, + }} + require.Equal(t, subjects, attest.Subject) + } +} + +func testExportAnnotations(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() + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) + + amd64 := platforms.MustParse("linux/amd64") + arm64 := platforms.MustParse("linux/arm64") + ps := []ocispecs.Platform{amd64, arm64} + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + expPlatforms := &exptypes.Platforms{ + Platforms: make([]exptypes.Platform, len(ps)), + } + for i, p := range ps { + st := scratch.File( + llb.Mkfile("platform", 0600, []byte(platforms.Format(p))), + ) + + 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 + } + + k := platforms.Format(p) + res.AddRef(k, ref) + + expPlatforms.Platforms[i] = exptypes.Platform{ + ID: k, + Platform: p, + } + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + res.AddMeta(exptypes.AnnotationIndexKey("gi"), []byte("generic index")) + res.AddMeta(exptypes.AnnotationIndexDescriptorKey("gid"), []byte("generic index descriptor")) + res.AddMeta(exptypes.AnnotationManifestKey(nil, "gm"), []byte("generic manifest")) + res.AddMeta(exptypes.AnnotationManifestDescriptorKey(nil, "gmd"), []byte("generic manifest descriptor")) + res.AddMeta(exptypes.AnnotationManifestKey(&amd64, "m"), []byte("amd64 manifest")) + res.AddMeta(exptypes.AnnotationManifestKey(&arm64, "m"), []byte("arm64 manifest")) + res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&amd64, "md"), []byte("amd64 manifest descriptor")) + res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&arm64, "md"), []byte("arm64 manifest descriptor")) + res.AddMeta(exptypes.AnnotationKey{Key: "gd"}.String(), []byte("generic default")) + + return res, nil + } + + // testing for image exporter + + target := registry + "/buildkit/testannotations:latest" + + const created = "2022-01-23T12:34:56Z" + + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "annotation-index.gio": "generic index opt", + "annotation-index." + ocispecs.AnnotationCreated: created, + "annotation-manifest.gmo": "generic manifest opt", + "annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt", + "annotation-manifest[linux/amd64].mo": "amd64 manifest opt", + "annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt", + "annotation-manifest[linux/arm64].mo": "arm64 manifest opt", + "annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + require.Equal(t, "generic index", imgs.Index.Annotations["gi"]) + require.Equal(t, "generic index opt", imgs.Index.Annotations["gio"]) + require.Equal(t, created, imgs.Index.Annotations[ocispecs.AnnotationCreated]) + for _, desc := range imgs.Index.Manifests { + require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"]) + require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"]) + switch { + case platforms.Only(amd64).Match(*desc.Platform): + require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"]) + require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"]) + case platforms.Only(arm64).Match(*desc.Platform): + require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"]) + require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"]) + default: + require.Fail(t, "unrecognized platform") + } + } + + amdImage := imgs.Find(platforms.Format(amd64)) + require.Equal(t, "generic default", amdImage.Manifest.Annotations["gd"]) + require.Equal(t, "generic manifest", amdImage.Manifest.Annotations["gm"]) + require.Equal(t, "generic manifest opt", amdImage.Manifest.Annotations["gmo"]) + require.Equal(t, "amd64 manifest", amdImage.Manifest.Annotations["m"]) + require.Equal(t, "amd64 manifest opt", amdImage.Manifest.Annotations["mo"]) + + armImage := imgs.Find(platforms.Format(arm64)) + require.Equal(t, "generic default", armImage.Manifest.Annotations["gd"]) + require.Equal(t, "generic manifest", armImage.Manifest.Annotations["gm"]) + require.Equal(t, "generic manifest opt", armImage.Manifest.Annotations["gmo"]) + require.Equal(t, "arm64 manifest", armImage.Manifest.Annotations["m"]) + require.Equal(t, "arm64 manifest opt", armImage.Manifest.Annotations["mo"]) + + // testing for oci exporter + + destDir := t.TempDir() + + out := filepath.Join(destDir, "out.tar") + outW, err := os.Create(out) + require.NoError(t, err) + + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Output: fixedWriteCloser(outW), + Attrs: map[string]string{ + "annotation-index.gio": "generic index opt", + "annotation-index-descriptor.gido": "generic index descriptor opt", + "annotation-index-descriptor." + ocispecs.AnnotationCreated: created, + "annotation-manifest.gmo": "generic manifest opt", + "annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt", + "annotation-manifest[linux/amd64].mo": "amd64 manifest opt", + "annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt", + "annotation-manifest[linux/arm64].mo": "arm64 manifest opt", + "annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt", + }, + }, + }, + }, "", frontend, 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 layout ocispecs.Index + err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &layout) + require.Equal(t, "generic index descriptor", layout.Manifests[0].Annotations["gid"]) + require.Equal(t, "generic index descriptor opt", layout.Manifests[0].Annotations["gido"]) + require.Equal(t, created, layout.Manifests[0].Annotations[ocispecs.AnnotationCreated]) + require.NoError(t, err) + + var index ocispecs.Index + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+layout.Manifests[0].Digest.Hex()].Data, &index) + require.Equal(t, "generic index", index.Annotations["gi"]) + require.Equal(t, "generic index opt", index.Annotations["gio"]) + require.NoError(t, err) + + for _, desc := range index.Manifests { + var mfst ocispecs.Manifest + err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+desc.Digest.Hex()].Data, &mfst) + require.NoError(t, err) + + require.Equal(t, "generic default", mfst.Annotations["gd"]) + require.Equal(t, "generic manifest", mfst.Annotations["gm"]) + require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"]) + require.Equal(t, "generic manifest opt", mfst.Annotations["gmo"]) + require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"]) + + switch { + case platforms.Only(amd64).Match(*desc.Platform): + require.Equal(t, "amd64 manifest", mfst.Annotations["m"]) + require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"]) + require.Equal(t, "amd64 manifest opt", mfst.Annotations["mo"]) + require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"]) + case platforms.Only(arm64).Match(*desc.Platform): + require.Equal(t, "arm64 manifest", mfst.Annotations["m"]) + require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"]) + require.Equal(t, "arm64 manifest opt", mfst.Annotations["mo"]) + require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"]) + default: + require.Fail(t, "unrecognized platform") + } + } +} + +func testExportAnnotationsMediaTypes(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) + + p := platforms.DefaultSpec() + ps := []ocispecs.Platform{p} + + scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + expPlatforms := &exptypes.Platforms{ + Platforms: make([]exptypes.Platform, len(ps)), + } + for i, p := range ps { + st := scratch.File( + llb.Mkfile("platform", 0600, []byte(platforms.Format(p))), + ) + + 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 + } + + k := platforms.Format(p) + res.AddRef(k, ref) + + expPlatforms.Platforms[i] = exptypes.Platform{ + ID: k, + Platform: p, + } + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + target := registry + "/buildkit/testannotationsmedia:1" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + "annotation-manifest.a": "b", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 1, len(imgs.Images)) + + target2 := registry + "/buildkit/testannotationsmedia:2" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target2, + "push": "true", + "annotation-index.c": "d", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target2) + require.NoError(t, err) + imgs2, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 1, len(imgs2.Images)) + + require.Equal(t, "b", imgs.Images[0].Manifest.Annotations["a"]) + require.Equal(t, "d", imgs2.Index.Annotations["c"]) + + require.Equal(t, ocispecs.MediaTypeImageIndex, imgs.Index.MediaType) + require.Equal(t, ocispecs.MediaTypeImageIndex, imgs2.Index.MediaType) +} + +func testExportAttestations(t *testing.T, sb integration.Sandbox, ociArtifact bool, setOCIArtifact bool) { + 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) + + ps := []ocispecs.Platform{ + platforms.MustParse("linux/amd64"), + platforms.MustParse("linux/arm64"), + } + + success := []byte(`{"success": true}`) + successDigest := digest.SHA256.FromBytes(success) + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + expPlatforms := &exptypes.Platforms{} + + for _, p := range ps { + pk := platforms.Format(p) + expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) + + // build image + st := llb.Scratch().File( + llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), + ) + 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(pk, ref) + + // build attestations + st = llb.Scratch(). + File(llb.Mkfile("/attestation.json", 0600, success)). + File(llb.Mkfile("/attestation2.json", 0600, []byte{})) + 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 + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_InToto, + Ref: refAttest, + Path: "/attestation.json", + InToto: result.InTotoAttestation{ + PredicateType: "https://example.com/attestations/v1.0", + Subjects: []result.InTotoSubject{{ + Kind: gatewaypb.InTotoSubjectKind_Self, + }}, + }, + }) + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_InToto, + Ref: refAttest, + Path: "/attestation2.json", + InToto: result.InTotoAttestation{ + PredicateType: "https://example.com/attestations2/v1.0", + Subjects: []result.InTotoSubject{{ + Kind: gatewaypb.InTotoSubjectKind_Raw, + Name: "/attestation.json", + Digest: []digest.Digest{successDigest}, + }}, + }, + }) + } + + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + t.Run("image", func(t *testing.T) { + expectedOCIArtifact := !setOCIArtifact || ociArtifact + targets := []string{ + registry + "/buildkit/testattestationsfoo:latest", + registry + "/buildkit/testattestationsbar:latest", + } + attrs := map[string]string{ + "name": strings.Join(targets, ","), + "push": "true", + } + if setOCIArtifact { + attrs["oci-artifact"] = strconv.FormatBool(ociArtifact) + } + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: attrs, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(targets[0]) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, len(ps)*2, len(imgs.Images)) + + var bases []*testutil.ImageInfo + for _, p := range ps { + pk := platforms.Format(p) + img := imgs.Find(pk) + require.NotNil(t, img) + require.Equal(t, pk, platforms.Format(*img.Desc.Platform)) + require.Equal(t, 1, len(img.Layers)) + require.Equal(t, fmt.Appendf(nil, "hello %s!", pk), img.Layers[0]["greeting"].Data) + bases = append(bases, img) + } + + atts := imgs.Filter("unknown/unknown") + require.Equal(t, len(ps), len(atts.Images)) + for i, att := range atts.Images { + require.Equal(t, ocispecs.MediaTypeImageManifest, att.Desc.MediaType) + require.Equal(t, "unknown/unknown", platforms.Format(*att.Desc.Platform)) + require.Equal(t, attestation.DockerAnnotationReferenceTypeDefault, att.Desc.Annotations[attestation.DockerAnnotationReferenceType]) + require.Equal(t, bases[i].Desc.Digest.String(), att.Desc.Annotations[attestation.DockerAnnotationReferenceDigest]) + require.Equal(t, 2, len(att.Layers)) + + if expectedOCIArtifact { + subject := att.Manifest.Subject + require.NotNil(t, subject) + require.Equal(t, bases[i].Desc.MediaType, subject.MediaType) + require.Equal(t, bases[i].Desc.Digest, subject.Digest) + require.Equal(t, bases[i].Desc.Size, subject.Size) + require.Empty(t, subject.Annotations) + require.Nil(t, subject.Platform) + require.Equal(t, "application/vnd.docker.attestation.manifest.v1+json", att.Manifest.ArtifactType) + require.Equal(t, ocispecs.DescriptorEmptyJSON, att.Manifest.Config) + } else { + require.Nil(t, att.Manifest.Subject) + require.Empty(t, att.Manifest.ArtifactType) + + // image config is not included in the OCI artifact + require.Equal(t, "unknown/unknown", att.Img.OS+"/"+att.Img.Architecture) + require.Equal(t, len(att.Layers), len(att.Img.RootFS.DiffIDs)) + require.Equal(t, 0, len(att.Img.History)) + } + + var attest intoto.Statement + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + + purls := map[string]string{} + for _, k := range targets { + named, err := reference.ParseNormalizedNamed(k) + require.NoError(t, err) + name := reference.FamiliarName(named) + version := "" + if tagged, ok := named.(reference.Tagged); ok { + version = tagged.Tag() + } + p := fmt.Sprintf("pkg:docker/%s%s@%s?platform=%s", url.QueryEscape(registry), strings.TrimPrefix(name, registry), version, url.PathEscape(platforms.Format(ps[i]))) + purls[k] = p + } + + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) + require.Equal(t, map[string]any{"success": true}, attest.Predicate) + subjects := []intoto.Subject{ + { + Name: purls[targets[0]], + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, + }, + { + Name: purls[targets[1]], + Digest: map[string]string{ + "sha256": bases[i].Desc.Digest.Encoded(), + }, + }, + } + require.Equal(t, subjects, attest.Subject) + + var attest2 intoto.Statement + require.NoError(t, json.Unmarshal(att.LayersRaw[1], &attest2)) + + require.Equal(t, intoto.StatementInTotoV1, attest2.Type) + require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) + require.Nil(t, attest2.Predicate) + subjects = []intoto.Subject{{ + Name: "/attestation.json", + Digest: map[string]string{ + "sha256": successDigest.Encoded(), + }, + }} + require.Equal(t, subjects, attest2.Subject) + } + + cdAddress := sb.ContainerdAddress() + if cdAddress == "" { + return + } + client, err := ctd.New(cdAddress) + require.NoError(t, err) + defer client.Close() + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + + for _, target := range targets { + err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) + require.NoError(t, err) + } + checkAllReleasable(t, c, sb, true) + }) + + t.Run("local", func(t *testing.T) { + dir := t.TempDir() + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: dir, + Attrs: map[string]string{ + "attestation-prefix": "test.", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + for _, p := range ps { + var attest intoto.Statement + dt, err := os.ReadFile(path.Join(dir, strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(dt, &attest)) + + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) + require.Equal(t, map[string]any{"success": true}, attest.Predicate) + + require.Equal(t, []intoto.Subject{{ + Name: "greeting", + Digest: result.ToDigestMap(digest.Canonical.FromString("hello " + platforms.Format(p) + "!")), + }}, attest.Subject) + + var attest2 intoto.Statement + dt, err = os.ReadFile(path.Join(dir, strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(dt, &attest2)) + + require.Equal(t, intoto.StatementInTotoV1, attest2.Type) + require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) + require.Nil(t, attest2.Predicate) + subjects := []intoto.Subject{{ + Name: "/attestation.json", + Digest: map[string]string{ + "sha256": successDigest.Encoded(), + }, + }} + require.Equal(t, subjects, attest2.Subject) + } + }) + + t.Run("tar", func(t *testing.T) { + dir := t.TempDir() + out := filepath.Join(dir, "out.tar") + outW, err := os.Create(out) + require.NoError(t, err) + + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterTar, + Output: fixedWriteCloser(outW), + Attrs: map[string]string{ + "attestation-prefix": "test.", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(out) + require.NoError(t, err) + + m, err := testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + for _, p := range ps { + var attest intoto.Statement + item := m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")] + require.NotNil(t, item) + require.NoError(t, json.Unmarshal(item.Data, &attest)) + + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) + require.Equal(t, map[string]any{"success": true}, attest.Predicate) + + require.Equal(t, []intoto.Subject{{ + Name: "greeting", + Digest: result.ToDigestMap(digest.Canonical.FromString("hello " + platforms.Format(p) + "!")), + }}, attest.Subject) + + var attest2 intoto.Statement + item = m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")] + require.NotNil(t, item) + require.NoError(t, json.Unmarshal(item.Data, &attest2)) + + require.Equal(t, intoto.StatementInTotoV1, attest2.Type) + require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) + require.Nil(t, attest2.Predicate) + subjects := []intoto.Subject{{ + Name: "/attestation.json", + Digest: map[string]string{ + "sha256": successDigest.Encoded(), + }, + }} + require.Equal(t, subjects, attest2.Subject) + } + }) +} + +func testExportAttestationsDefaultOCIArtifact(t *testing.T, sb integration.Sandbox) { + testExportAttestations(t, sb, false, false) +} + +func testExportAttestationsImageManifest(t *testing.T, sb integration.Sandbox) { + testExportAttestations(t, sb, false, true) +} + +func testExportAttestationsOCIArtifact(t *testing.T, sb integration.Sandbox) { + testExportAttestations(t, sb, true, true) +} + +func testImageResolveAttestationChainLocal(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) + requiresLinux(t) + + ctx := sb.Context() + c, err := New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + target, platform := buildProvenanceImage(ctx, t, c, sb) + + _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ + Identifier: "docker-image://" + target, + }, sourceresolver.Opt{ + ImageOpt: &sourceresolver.ResolveImageOpt{ + NoConfig: true, + AttestationChain: true, + Platform: &platform, + ResolveMode: pb.AttrImageResolveModeForcePull, + }, + }) + if err != nil { + return nil, err + } + require.NotNil(t, md.Image) + require.NotNil(t, md.Image.AttestationChain) + ac := md.Image.AttestationChain + require.NotEmpty(t, ac.AttestationManifest) + att := ac.Blobs[ac.AttestationManifest] + require.NotEmpty(t, att.Data) + + var manifest ocispecs.Manifest + require.NoError(t, json.Unmarshal(att.Data, &manifest)) + require.NotEmpty(t, manifest.Layers) + found := false + for _, layer := range manifest.Layers { + if isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) { + found = true + break + } + } + require.True(t, found) + return nil, nil + }, nil) + require.NoError(t, err) +} + +func testImageResolveAttestationChainRequiresNetwork(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureMultiPlatform, workers.FeatureProvenance) + // this test temporarily requires direct registry access as the integration test + // mirroring system does not support mirroring attestation chains yet. + // Support is coming in future buildx release. + ctx := sb.Context() + c, err := New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + amd64, err := platforms.Parse("linux/amd64") + require.NoError(t, err) + + _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + const rootDigest = "sha256:4e91099af134f4b1f509fecbd1a55981dfff18d8029d6782e28413210ef468b7" + const imageDigest = "sha256:d2f0b39234a66c3d58f24200d3fda9e4e3d2263c09f9b7286a826ab639713047" + const attestationDigest = "sha256:fb4c46b14f52d1bf790f593921c52dddc698fc6780792fb8469fc60efc0e609b" + const sigDigest = "sha256:bd0a6b088440ba9838e8eec79e736128fe52afce934578eae30ca8675f6d3142" + + id := "registry-1-stage.docker.io/docker/github-builder-test@" + rootDigest + md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ + Identifier: "docker-image://" + id, + }, sourceresolver.Opt{ + ImageOpt: &sourceresolver.ResolveImageOpt{ + NoConfig: true, + AttestationChain: true, + Platform: &amd64, + }, + }) + if err != nil { + return nil, err + } + require.Equal(t, rootDigest, md.Image.Digest.String()) + require.Nil(t, md.Image.Config) + require.NotNil(t, md.Image) + require.NotNil(t, md.Image.AttestationChain) + ac := md.Image.AttestationChain + require.Equal(t, rootDigest, ac.Root.String()) + require.Equal(t, imageDigest, ac.ImageManifest.String()) + require.Equal(t, attestationDigest, ac.AttestationManifest.String()) + require.Len(t, ac.SignatureManifests, 1) + require.Equal(t, sigDigest, ac.SignatureManifests[0].String()) + + desc := ac.Blobs[ac.Root] + require.Equal(t, rootDigest, desc.Descriptor.Digest.String()) + require.Len(t, desc.Data, int(desc.Descriptor.Size)) + require.Equal(t, ocispecs.MediaTypeImageIndex, desc.Descriptor.MediaType) + chk := digest.FromBytes(desc.Data) + require.Equal(t, rootDigest, chk.String()) + + desc = ac.Blobs[ac.ImageManifest] + // image manifest is expected to be missing as content is not needed to verify attestation chain + _, ok := ac.Blobs[digest.Digest(imageDigest)] + require.False(t, ok) + + desc = ac.Blobs[ac.AttestationManifest] + require.Equal(t, attestationDigest, desc.Descriptor.Digest.String()) + require.Equal(t, ocispecs.MediaTypeImageManifest, desc.Descriptor.MediaType) + require.Len(t, desc.Data, int(desc.Descriptor.Size)) + chk = digest.FromBytes(desc.Data) + require.Equal(t, attestationDigest, chk.String()) + + desc = ac.Blobs[ac.SignatureManifests[0]] + require.Equal(t, sigDigest, desc.Descriptor.Digest.String()) + require.Equal(t, ocispecs.MediaTypeImageManifest, desc.Descriptor.MediaType) + require.Len(t, desc.Data, int(desc.Descriptor.Size)) + chk = digest.FromBytes(desc.Data) + require.Equal(t, sigDigest, chk.String()) + + var sigMfst ocispecs.Manifest + err = json.Unmarshal(ac.Blobs[ac.SignatureManifests[0]].Data, &sigMfst) + require.NoError(t, err) + require.Equal(t, 1, len(sigMfst.Layers)) + sigLayer := sigMfst.Layers[0] + sigDesc := ac.Blobs[digest.Digest(sigLayer.Digest)] + require.Equal(t, sigLayer.Digest, sigDesc.Descriptor.Digest) + require.Len(t, sigDesc.Data, int(sigDesc.Descriptor.Size)) + require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", sigDesc.Descriptor.MediaType) + chk = digest.FromBytes(sigDesc.Data) + require.Equal(t, sigLayer.Digest, chk) + + require.Len(t, md.Image.AttestationChain.Blobs, 4) + return nil, nil + }, nil) + require.NoError(t, err) +} + +func testImageResolveProvenanceAttestation(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) + requiresLinux(t) + + ctx := sb.Context() + c, err := New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + target, platform := buildProvenanceImage(ctx, t, c, sb) + + _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ + Identifier: "docker-image://" + target, + }, sourceresolver.Opt{ + ImageOpt: &sourceresolver.ResolveImageOpt{ + NoConfig: true, + ResolveAttestations: []string{ + policyimage.SLSAProvenancePredicateType02, + policyimage.SLSAProvenancePredicateType1, + }, + Platform: &platform, + ResolveMode: pb.AttrImageResolveModeForcePull, + }, + }) + if err != nil { + return nil, err + } + require.NotNil(t, md.Image) + require.NotNil(t, md.Image.AttestationChain) + ac := md.Image.AttestationChain + require.NotEmpty(t, ac.AttestationManifest) + att := ac.Blobs[ac.AttestationManifest] + require.NotEmpty(t, att.Data) + + var manifest ocispecs.Manifest + require.NoError(t, json.Unmarshal(att.Data, &manifest)) + require.NotEmpty(t, manifest.Layers) + var ( + stmtBytes []byte + foundLayer ocispecs.Descriptor + ) + for _, layer := range manifest.Layers { + if !isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) { + continue + } + blob, ok := ac.Blobs[layer.Digest] + if !ok { + continue + } + stmtBytes = blob.Data + foundLayer = layer + break + } + require.NotEmpty(t, stmtBytes) + require.Contains(t, []string{ + policyimage.SLSAProvenancePredicateType02, + policyimage.SLSAProvenancePredicateType1, + }, foundLayer.Annotations["in-toto.io/predicate-type"]) + + var stmt intoto.Statement + require.NoError(t, json.Unmarshal(stmtBytes, &stmt)) + require.Equal(t, intoto.StatementInTotoV1, stmt.Type) + require.Contains(t, []string{ + policyimage.SLSAProvenancePredicateType02, + policyimage.SLSAProvenancePredicateType1, + }, stmt.PredicateType) + require.Equal(t, stmt.Subject[0].Digest["sha256"], ac.ImageManifest.Hex()) + return nil, nil + }, nil) + require.NoError(t, err) +} + +func testSBOMScan(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) + 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) + + p := platforms.MustParse("linux/amd64") + pk := platforms.Format(p) + + scannerFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + st := llb.Image("busybox", llb.Platform(p)) + def, err := st.Marshal(sb.Context()) + require.NoError(t, 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.SetRef(ref) + + var img ocispecs.Image + cmd := ` +cat < $BUILDKIT_SCAN_DESTINATION/spdx.json +{ + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://spdx.dev/Document", + "predicate": { + "name": "fallback", + "extraParams": { + "ARG1": "$BUILDKIT_SCAN_ARG1", + "ARG2": "$BUILDKIT_SCAN_ARG2" + } + } +} +EOF +` + img.Config.Cmd = []string{"/bin/sh", "-c", cmd} + img.Platform = p + config, err := json.Marshal(img) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal image config") + } + res.AddMeta(exptypes.ExporterImageConfigKey, config) + + return res, nil + } + + scannerTarget := registry + "/buildkit/testsbomscanner:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": scannerTarget, + "push": "true", + "oci-mediatypes": "false", + }, + }, + }, + }, "", scannerFrontend, nil) + require.NoError(t, err) + + makeTargetFrontend := func(attest bool) func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + return func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + // build image + st := llb.Scratch().File( + llb.Mkfile("/greeting", 0600, []byte("hello world!")), + ) + 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(pk, ref) + + expPlatforms := &exptypes.Platforms{ + Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + // build attestations + if attest { + st = llb.Scratch(). + File(llb.Mkfile("/result.spdx", 0600, []byte(`{"name": "frontend"}`))) + 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 + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_InToto, + Ref: refAttest, + Path: "/result.spdx", + InToto: result.InTotoAttestation{ + PredicateType: intoto.PredicateSPDX, + }, + }) + } + + return res, nil + } + } + + // test the default fallback scanner + target := registry + "/buildkit/testsbom:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(false), nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + // test the frontend builtin scanner + target = registry + "/buildkit/testsbom2:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(true), nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att := imgs.Find("unknown/unknown") + attest := intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{"name": "frontend"}) + + // test the specified fallback scanner + target = registry + "/buildkit/testsbom3:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "generator=" + scannerTarget, + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(false), nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att = imgs.Find("unknown/unknown") + attest = intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{"name": "fallback"}) + + // test the builtin frontend scanner and the specified fallback scanner together + target = registry + "/buildkit/testsbom3:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "generator=" + scannerTarget, + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(true), nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att = imgs.Find("unknown/unknown") + attest = intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{"name": "frontend"}) + + // test configuring the scanner (simple) + target = registry + "/buildkit/testsbom4:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "generator=" + scannerTarget + ",ARG1=foo,ARG2=bar", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(false), nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att = imgs.Find("unknown/unknown") + attest = intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{ + "extraParams": map[string]any{"ARG1": "foo", "ARG2": "bar"}, + }) + + // test configuring the scanner (complex) + target = registry + "/buildkit/testsbom4:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "\"generator=" + scannerTarget + "\",\"ARG1=foo\",\"ARG2=hello,world\"", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", makeTargetFrontend(false), nil) + require.NoError(t, err) + + desc, provider, err = contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err = testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att = imgs.Find("unknown/unknown") + attest = intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{ + "extraParams": map[string]any{"ARG1": "foo", "ARG2": "hello,world"}, + }) +} + +func testSBOMScanSingleRef(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) + 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) + + p := platforms.DefaultSpec() + pk := platforms.Format(p) + + scannerFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + st := llb.Image("busybox", llb.Platform(p)) + def, err := st.Marshal(sb.Context()) + require.NoError(t, 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.SetRef(ref) + + var img ocispecs.Image + cmd := ` +cat < $BUILDKIT_SCAN_DESTINATION/spdx.json +{ + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://spdx.dev/Document", + "predicate": {"name": "fallback"} +} +EOF +` + img.Config.Cmd = []string{"/bin/sh", "-c", cmd} + img.Platform = p + config, err := json.Marshal(img) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal image config") + } + res.AddMeta(exptypes.ExporterImageConfigKey, config) + + return res, nil + } + + scannerTarget := registry + "/buildkit/testsbomscanner:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": scannerTarget, + "push": "true", + "oci-mediatypes": "false", + }, + }, + }, + }, "", scannerFrontend, nil) + require.NoError(t, err) + + targetFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + // build image + st := llb.Scratch().File( + llb.Mkfile("/greeting", 0600, []byte("hello world!")), + ) + 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.SetRef(ref) + + var img ocispecs.Image + img.Config.Cmd = []string{"/bin/sh", "-c", "cat /greeting"} + img.Platform = p + config, err := json.Marshal(img) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal image config") + } + res.AddMeta(exptypes.ExporterImageConfigKey, config) + + expPlatforms := &exptypes.Platforms{ + Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + target := registry + "/buildkit/testsbomsingle:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "generator=" + scannerTarget, + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", targetFrontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + var img *testutil.ImageInfo + for _, candidate := range imgs.Images { + if candidate.Desc.Platform != nil && platforms.Format(*candidate.Desc.Platform) == "unknown/unknown" { + continue + } + img = candidate + break + } + require.NotNil(t, img) + require.Equal(t, []string{"/bin/sh", "-c", "cat /greeting"}, img.Img.Config.Cmd) + + att := imgs.Find("unknown/unknown") + require.NotNil(t, att) + attest := intoto.Statement{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + require.Subset(t, attest.Predicate, map[string]any{"name": "fallback"}) +} + +func testSBOMSupplements(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) + 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) + + p := platforms.MustParse("linux/amd64") + pk := platforms.Format(p) + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + // build image + st := llb.Scratch().File( + llb.Mkfile("/foo", 0600, []byte{}), + ) + 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(pk, ref) + + expPlatforms := &exptypes.Platforms{ + Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + // build attestations + doc := spdx.Document{ + SPDXVersion: "SPDX-2.2", + SPDXIdentifier: "DOCUMENT", + Files: []*spdx.File{ + { + // foo exists... + FileSPDXIdentifier: "SPDXRef-File-foo", + FileName: "/foo", + }, + { + // ...but bar doesn't + FileSPDXIdentifier: "SPDXRef-File-bar", + FileName: "/bar", + }, + }, + } + docBytes, err := json.Marshal(doc) + if err != nil { + return nil, err + } + st = llb.Scratch(). + File(llb.Mkfile("/result.spdx", 0600, docBytes)) + 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 + } + refAttest, err := r.SingleRef() + if err != nil { + return nil, err + } + _, err = ref.ToState() + if err != nil { + return nil, err + } + + res.AddAttestation(pk, gateway.Attestation{ + Kind: gatewaypb.AttestationKind_InToto, + Ref: refAttest, + Path: "/result.spdx", + InToto: result.InTotoAttestation{ + PredicateType: intoto.PredicateSPDX, + }, + Metadata: map[string][]byte{ + result.AttestationSBOMCore: []byte("result"), + }, + }) + + return res, nil + } + + // test the default fallback scanner + target := registry + "/buildkit/testsbom:latest" + _, err = c.Build(sb.Context(), SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:sbom": "", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + desc, provider, err := contentutil.ProviderFromRef(target) + require.NoError(t, err) + + imgs, err := testutil.ReadImages(sb.Context(), provider, desc) + require.NoError(t, err) + require.Equal(t, 2, len(imgs.Images)) + + att := imgs.Find("unknown/unknown") + attest := struct { + intoto.StatementHeader + Predicate spdx.Document + }{} + require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) + require.Equal(t, intoto.StatementInTotoV1, attest.Type) + require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) + + require.Equal(t, "DOCUMENT", string(attest.Predicate.SPDXIdentifier)) + require.Len(t, attest.Predicate.Files, 2) + require.Equal(t, "/foo", attest.Predicate.Files[0].FileName) + require.Regexp(t, "^layerID: sha256:", attest.Predicate.Files[0].FileComment) + require.Equal(t, "/bar", attest.Predicate.Files[1].FileName) + require.Empty(t, attest.Predicate.Files[1].FileComment) + + checkAllReleasable(t, c, sb, true) +} + +func testSourceDateEpochClamp(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) + requiresLinux(t) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + var bboxConfig []byte + _, err = c.Build(sb.Context(), SolveOpt{}, "", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + _, _, bboxConfig, err = c.ResolveImageConfig(ctx, "docker.io/library/busybox:latest", sourceresolver.Opt{}) + if err != nil { + return nil, err + } + return nil, nil + }, nil) + require.NoError(t, err) + + m := map[string]json.RawMessage{} + require.NoError(t, json.Unmarshal(bboxConfig, &m)) + delete(m, "created") + bboxConfig, err = json.Marshal(m) + require.NoError(t, err) + + busybox, err := llb.Image("busybox:latest").WithImageConfig(bboxConfig) + require.NoError(t, err) + + def, err := busybox.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, + Attrs: map[string]string{ + exptypes.ExporterImageConfigKey: string(bboxConfig), + }, + Output: fixedWriteCloser(outW), + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(out) + require.NoError(t, err) + + busyboxTmsX, err := readImageTimestamps(dt) + require.NoError(t, err) + busyboxTms := busyboxTmsX.FromImage + + require.Greater(t, len(busyboxTms), 1) + bboxLayerLen := len(busyboxTms) - 1 + + tm, err := time.Parse(time.RFC3339Nano, busyboxTms[1]) + require.NoError(t, err) + + next := tm.Add(time.Hour).Truncate(time.Second) + + st := busybox.Run(llb.Shlex("touch /foo")) + + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + out = filepath.Join(destDir, "out.tar") + outW, err = os.Create(out) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", next.Unix()), + }, + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Attrs: map[string]string{ + exptypes.ExporterImageConfigKey: string(bboxConfig), + }, + Output: fixedWriteCloser(outW), + }, + }, + }, nil) + require.NoError(t, err) + + dt, err = os.ReadFile(out) + require.NoError(t, err) + + tmsX, err := readImageTimestamps(dt) + require.NoError(t, err) + tms := tmsX.FromImage + + require.Equal(t, len(tms), bboxLayerLen+2) + + expected := next.UTC().Format(time.RFC3339Nano) + require.Equal(t, expected, tms[0]) + require.Equal(t, busyboxTms[1], tms[1]) + require.Equal(t, expected, tms[bboxLayerLen+1]) + require.Equal(t, expected, tmsX.FromAnnotation) + + checkAllReleasable(t, c, sb, true) +} + +func testSourceDateEpochImageExporter(t *testing.T, sb integration.Sandbox) { + cdAddress := sb.ContainerdAddress() + if cdAddress == "" { + t.SkipNow() + } + + // https://github.com/containerd/containerd/commit/133ddce7cf18a1db175150e7a69470dea1bb3132 + minVer := "v1.7.0-beta.1" + cdVersion := containerdutil.GetVersion(t, cdAddress) + // Normalize containerd version to semver format (add v prefix if missing) + normalizedCdVersion := cdVersion + if !strings.HasPrefix(cdVersion, "v") { + normalizedCdVersion = "v" + cdVersion + } + if semver.Compare(normalizedCdVersion, minVer) < 0 { + t.Skipf("containerd version %q does not satisfy minimal version %q", cdVersion, minVer) + } + + workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + + type testVars struct { + baseImgName string + st llb.State + cmdPrefix string + } + + baseImg := llb.Image(integration.UnixOrWindows("busybox:latest", "nanoserver:latest")) + v := integration.UnixOrWindows( + testVars{ + baseImgName: "busybox:latest", + st: llb.Scratch(), + cmdPrefix: `sh -c "echo -n`, + }, + testVars{ + baseImgName: "nanoserver:latest", + st: baseImg, + cmdPrefix: `cmd /C "echo`, + }, + ) + + st := v.st + run := func(cmd string) { + st = baseImg.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + } + + run(fmt.Sprintf(`%s first> foo"`, v.cmdPrefix)) + run(fmt.Sprintf(`%s second> bar"`, v.cmdPrefix)) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + name := strings.ToLower(path.Base(t.Name())) + tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": name, + }, + }, + }, + }, nil) + require.NoError(t, err) + + ctx := namespaces.WithNamespace(sb.Context(), "buildkit") + client, err := newContainerd(cdAddress) + require.NoError(t, err) + defer client.Close() + + img, err := client.GetImage(ctx, name) + require.NoError(t, err) + require.Equal(t, tm, img.Metadata().CreatedAt) + + err = client.ImageService().Delete(ctx, name, images.SynchronousDelete()) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) +} + +func testSourceDateEpochLayerTimestamps(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) + 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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + 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) + + tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), + }, + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Output: fixedWriteCloser(outW), + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(out) + require.NoError(t, err) + + tmsX, err := readImageTimestamps(dt) + require.NoError(t, err) + tms := tmsX.FromImage + + require.Equal(t, 3, len(tms)) + + expected := tm.UTC().Format(time.RFC3339Nano) + require.Equal(t, expected, tms[0]) + require.Equal(t, expected, tms[1]) + require.Equal(t, expected, tms[2]) + + checkAllReleasable(t, c, sb, true) +} + +func testSourceDateEpochLocalExporter(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) + 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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + destDir, err := os.MkdirTemp("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(destDir) + + tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), + }, + 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, fi.ModTime().Format(time.RFC3339), tm.UTC().Format(time.RFC3339)) + + fi, err = os.Stat(filepath.Join(destDir, "bar")) + require.NoError(t, err) + require.Equal(t, fi.ModTime().Format(time.RFC3339), tm.UTC().Format(time.RFC3339)) + + checkAllReleasable(t, c, sb, true) +} + +// testSourceDateEpochReset tests that the SOURCE_DATE_EPOCH is reset if exporter option is set +func testSourceDateEpochReset(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) + 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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + 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) + + tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), + }, + Exports: []ExportEntry{ + { + Type: ExporterOCI, + Attrs: map[string]string{"source-date-epoch": ""}, + Output: fixedWriteCloser(outW), + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(out) + require.NoError(t, err) + + tmsX, err := readImageTimestamps(dt) + require.NoError(t, err) + tms := tmsX.FromImage + + require.Equal(t, 3, len(tms)) + + expected := tm.UTC().Format(time.RFC3339Nano) + require.NotEqual(t, expected, tms[0]) + require.NotEqual(t, expected, tms[1]) + require.NotEqual(t, expected, tms[2]) + + require.Equal(t, tms[0], tms[2]) + require.NotEqual(t, tms[2], tms[1]) + + checkAllReleasable(t, c, sb, true) +} + +func testSourceDateEpochTarExporter(t *testing.T, sb integration.Sandbox) { + workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) + 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 "echo -n first > foo"`) + run(`sh -c "echo -n second > bar"`) + + 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) + + tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + FrontendAttrs: map[string]string{ + "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), + }, + 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) + + require.Equal(t, 2, len(m)) + + require.Equal(t, tm.Format(time.RFC3339), m["foo"].Header.ModTime.Format(time.RFC3339)) + require.Equal(t, tm.Format(time.RFC3339), m["bar"].Header.ModTime.Format(time.RFC3339)) + + checkAllReleasable(t, c, sb, true) +} + +func buildProvenanceImage(ctx context.Context, t *testing.T, c *Client, sb integration.Sandbox) (string, ocispecs.Platform) { + t.Helper() + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + + platform := platforms.Normalize(platforms.DefaultSpec()) + platformKey := platforms.Format(platform) + target := registry + "/buildkit/testprovenance:latest" + + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res := gateway.NewResult() + + st := llb.Scratch().File( + llb.Mkfile("/greeting", 0600, []byte("hello provenance")), + ) + 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(platformKey, ref) + + img := ocispecs.Image{ + Platform: platform, + } + config, err := json.Marshal(img) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal image config") + } + res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, platformKey), config) + + expPlatforms := &exptypes.Platforms{ + Platforms: []exptypes.Platform{{ID: platformKey, Platform: platform}}, + } + dt, err := json.Marshal(expPlatforms) + if err != nil { + return nil, err + } + res.AddMeta(exptypes.ExporterPlatformsKey, dt) + + return res, nil + } + + _, err = c.Build(ctx, SolveOpt{ + FrontendAttrs: map[string]string{ + "attest:provenance": "mode=max,version=v1", + }, + Exports: []ExportEntry{ + { + Type: ExporterImage, + Attrs: map[string]string{ + "name": target, + "push": "true", + }, + }, + }, + }, "", frontend, nil) + require.NoError(t, err) + + return target, platform +} + +// isSLSAPredicateType reports whether the predicate type represents SLSA provenance. +func isSLSAPredicateType(v string) bool { + switch v { + case policyimage.SLSAProvenancePredicateType02, policyimage.SLSAProvenancePredicateType1: + return true + default: + return false + } +} diff --git a/client/client_fileop_test.go b/client/client_fileop_test.go new file mode 100644 index 000000000..b94c5987b --- /dev/null +++ b/client/client_fileop_test.go @@ -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) +} diff --git a/client/client_frontend_test.go b/client/client_frontend_test.go new file mode 100644 index 000000000..2c500acb9 --- /dev/null +++ b/client/client_frontend_test.go @@ -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") +} diff --git a/client/client_git_source_test.go b/client/client_git_source_test.go new file mode 100644 index 000000000..f757c0f2e --- /dev/null +++ b/client/client_git_source_test.go @@ -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 +} diff --git a/client/client_http_source_test.go b/client/client_http_source_test.go new file mode 100644 index 000000000..1dbb3a464 --- /dev/null +++ b/client/client_http_source_test.go @@ -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) +} diff --git a/client/client_image_source_test.go b/client/client_image_source_test.go new file mode 100644 index 000000000..1c680b445 --- /dev/null +++ b/client/client_image_source_test.go @@ -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 + } +} diff --git a/client/client_local_source_test.go b/client/client_local_source_test.go new file mode 100644 index 000000000..04fded9d8 --- /dev/null +++ b/client/client_local_source_test.go @@ -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) +} diff --git a/client/client_mergeop_test.go b/client/client_mergeop_test.go new file mode 100644 index 000000000..b5c8be6ee --- /dev/null +++ b/client/client_mergeop_test.go @@ -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, " && ")})) +} diff --git a/client/client_mount_test.go b/client/client_mount_test.go new file mode 100644 index 000000000..c5deb2300 --- /dev/null +++ b/client/client_mount_test.go @@ -0,0 +1,1080 @@ +package client + +import ( + "archive/tar" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "runtime" + "sync/atomic" + "testing" + "time" + + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/session" + "github.com/moby/buildkit/session/secrets/secretsprovider" + "github.com/moby/buildkit/session/sshforward/sshprovider" + "github.com/moby/buildkit/util/testutil" + "github.com/moby/buildkit/util/testutil/integration" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh/agent" +) + +func testBuildMultiMount(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + imgAlpine := integration.UnixOrWindows( + "docker.io/library/alpine:latest", + "nanoserver", + ) + imgBusybox := integration.UnixOrWindows( + "docker.io/library/busybox:latest", + "nanoserver", + ) + cmdStr1 := integration.UnixOrWindows( + "/bin/ls -l", + "cmd /C dir", + ) + cmdStr2 := integration.UnixOrWindows( + "/bin/cp -a /busybox/etc/passwd baz", + `cmd /C copy C:\\busybox\\License.txt baz`, + ) + + alpine := llb.Image(imgAlpine) + ls := alpine.Run(llb.Shlex(cmdStr1)) + busybox := llb.Image(imgBusybox) + cp := ls.Run(llb.Shlex(cmdStr2)) + cp.AddMount("/busybox", busybox) + + def, err := cp.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) +} + +func testCachedMounts(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)) + } + + busybox := llb.Image(imgName) + // setup base for one of the cache sources + cmdPrefix := integration.UnixOrWindows( + `sh -c "echo -n`, + `cmd /C "echo`, + ) + st := busybox.Run(llb.Shlexf(`%s base > baz"`, cmdPrefix), llb.Dir("/wd")) + base := st.AddMount("/wd", scratch()) + + st = busybox.Run(llb.Shlexf(`%s first > foo"`, cmdPrefix), llb.Dir("/wd")) + + st.AddMount("/wd", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + cmdStr := integration.UnixOrWindows( + `sh -c "cat foo && echo -n second > /wd2/bar"`, + `cmd /C type foo && echo second > C:\\wd2\\bar`, + ) + st = st.Run(llb.Shlex(cmdStr), llb.Dir("/wd")) + st.AddMount("/wd", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + st.AddMount("/wd2", base, llb.AsPersistentCacheDir("mycache2", llb.CacheMountShared)) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + // repeat to make sure cache works + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + // second build using cache directories + cmdStr = integration.UnixOrWindows( + `sh -c "cp /src0/foo . && cp /src1/bar . && cp /src1/baz ."`, + `cmd /C copy C:\\src0\\foo . && copy C:\\src1\\bar . && copy C:\\src1\\baz .`, + ) + st = busybox.Run(llb.Shlex(cmdStr), llb.Dir("/wd")) + out := st.AddMount("/wd", scratch()) + st.AddMount("/src0", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + st.AddMount("/src1", base, llb.AsPersistentCacheDir("mycache2", llb.CacheMountShared)) + + destDir := t.TempDir() + + def, err = out.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) + + newLine := integration.UnixOrWindows("", " \r\n") + dt, err := os.ReadFile(filepath.Join(destDir, "foo")) + require.NoError(t, err) + require.Equal(t, "first"+newLine, string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "bar")) + require.NoError(t, err) + require.Equal(t, "second"+newLine, string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "baz")) + require.NoError(t, err) + require.Equal(t, "base"+newLine, string(dt)) + + checkAllReleasable(t, c, sb, true) +} + +func testCacheMountNoCache(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") + + out := busybox.Run(llb.Shlex(`sh -e -c "touch /m1/foo; touch /m2/bar"`)) + out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + out.AddMount("/m2", llb.Scratch(), llb.AsPersistentCacheDir("mycache2", llb.CacheMountLocked)) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + out = busybox.Run(llb.Shlex(`sh -e -c "[[ ! -f /m1/foo ]]; touch /m1/foo2;"`), llb.IgnoreCache) + out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + + def, err = out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + out = busybox.Run(llb.Shlex(`sh -e -c "[[ -f /m1/foo2 ]]; [[ -f /m2/bar ]];"`)) + out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + out.AddMount("/m2", llb.Scratch(), llb.AsPersistentCacheDir("mycache2", llb.CacheMountLocked)) + + def, err = out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testDuplicateCacheMount(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) + scratch := func() llb.State { + return integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) + } + + cmdStr := integration.UnixOrWindows( + `sh -e -c "[[ ! -f /m2/foo ]]; touch /m1/foo; [[ -f /m2/foo ]];"`, + `cmd /C echo a > \\m1\\foo && dir \\m2\\foo`, + ) + out := busybox.Run(llb.Shlex(cmdStr)) + out.AddMount("/m1", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + out.AddMount("/m2", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testLayerLimitOnMounts(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() + + base := llb.Image("busybox:latest") + + const numLayers = 110 + + for range numLayers { + base = base.Run(llb.Shlex("sh -c 'echo hello >> /hello'")).Root() + } + + def, err := base.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(ctx, def, SolveOpt{}, nil) + require.NoError(t, err) + + ls := llb.Image("busybox:latest"). + Run(llb.Shlexf("ls -l /base/hello")) + ls.AddMount("/base", base, llb.Readonly) + + def, err = ls.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(ctx, def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testLLBMountPerformance(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") + srcPath := integration.UnixOrWindows("/bin", "/Windows") + + mntInput := llb.Image(imgName) + st := llb.Image(imgName) + var mnts []llb.State + // Reduce iterations on Windows due to significantly slower container operations + numIterations := integration.UnixOrWindows(20, 5) + for range numIterations { + execSt := st.Run( + llb.Args(integration.UnixOrWindows( + []string{"true"}, + []string{"cmd", "/C", "exit 0"}, + )), + ) + mnts = append(mnts, mntInput) + for j := range mnts { + mnts[j] = execSt.AddMount(fmt.Sprintf("/tmp/bin%d", j), mnts[j], llb.SourcePath(srcPath)) + } + st = execSt.Root() + } + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + // Windows images take longer time, especially on CI systems + // With reduced iterations (5 vs 20), use generous timeout + timeout := integration.UnixOrWindows(time.Minute, 10*time.Minute) + timeoutCtx, cancel := context.WithTimeoutCause(sb.Context(), timeout, nil) + defer cancel() + _, err = c.Solve(timeoutCtx, def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testLockedCacheMounts(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; if [[ -f two ]]; then exit 0; fi; for i in $(seq 10); do if [[ -f two ]]; then exit 1; fi; usleep 200000; done"`), llb.Dir("/wd")) + st.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + + st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; if [[ -f one ]]; then exit 0; fi; for i in $(seq 10); do if [[ -f one ]]; then exit 1; fi; usleep 200000; done"`), llb.Dir("/wd")) + st2.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) + + out := busybox.Run(llb.Shlex("true")) + out.AddMount("/m1", st.Root()) + out.AddMount("/m2", st2.Root()) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +// testMountStubsDirectory verifies that BuildKit cleans up stub directories created for tmpfs +// mounts after a Run step completes. Empty stubs should be removed, stubs with user content +// should be kept, and pre-existing directories used as mount targets should be preserved. +func testMountStubsDirectory(t *testing.T, sb integration.Sandbox) { + // Skipped on Windows because the test relies on tmpfs mounts, multiple simultaneous AddMount + // calls, and llb.Scratch() — none of which are supported on Windows containers. The stub + // cleanup behavior is also Linux-specific (tied to overlayfs mount point handling). + integration.SkipOnPlatform(t, "windows", "requires tmpfs, multiple mounts, and Scratch — all unsupported on Windows") + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + st := llb.Image("busybox:latest"). + File(llb.Mkdir("/test", 0700)). + File(llb.Mkdir("/test/qux/", 0700)). + Run( + llb.Args([]string{"touch", "/test/baz/keep"}), + // check stubs directory is removed + llb.AddMount("/test/foo", llb.Scratch(), llb.Tmpfs()), + // check that stubs directory are recursively removed + llb.AddMount("/test/bar/x/y", llb.Scratch(), llb.Tmpfs()), + // check that only empty stubs directories are removed + llb.AddMount("/test/baz/x", llb.Scratch(), llb.Tmpfs()), + // check that previously existing directory are not removed + llb.AddMount("/test/qux", llb.Scratch(), llb.Tmpfs()), + ).Root() + st = llb.Scratch().File(llb.Copy(st, "/test", "/", &llb.CopyInfo{CopyDirContentsOnly: true})) + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + tmpDir := t.TempDir() + tarFile := filepath.Join(tmpDir, "out.tar") + tarFileW, err := os.Create(tarFile) + require.NoError(t, err) + defer tarFileW.Close() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterTar, + Output: fixedWriteCloser(tarFileW), + }, + }, + }, nil) + require.NoError(t, err) + tarFileW.Close() + + dt, err := os.ReadFile(tarFile) + require.NoError(t, err) + + m, err := testutil.ReadTarToMap(dt, false) + require.NoError(t, err) + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + require.ElementsMatch(t, []string{ + "baz/", + "baz/keep", + "qux/", + }, keys) +} + +// testMountStubsTimestamp verifies that timestamps set on directories used as mount points +// (and their parents) are preserved after the mount is removed. +// https://github.com/moby/buildkit/issues/3148 +func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) { + // Skipped on Windows because the test relies on tmpfs mounts, multiple AddMount calls, + // Scratch, and Linux-specific commands — all unsupported on Windows containers. + integration.SkipOnPlatform(t, "windows", "requires tmpfs, multiple mounts, and Scratch — all unsupported on Windows") + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + const sourceDateEpoch = int64(1234567890) // Fri Feb 13 11:31:30 PM UTC 2009 + st := llb.Image("busybox:latest").Run( + llb.Args([]string{ + "/bin/touch", fmt.Sprintf("--date=@%d", sourceDateEpoch), + "/bin", + "/etc", + "/var", + "/var/foo", + "/tmp", + "/tmp/foo2", + "/tmp/foo2/bar", + }), + llb.AddMount("/var/foo", llb.Scratch(), llb.Tmpfs()), + llb.AddMount("/tmp/foo2/bar", llb.Scratch(), llb.Tmpfs()), + ) + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + tmpDir := t.TempDir() + tarFile := filepath.Join(tmpDir, "out.tar") + tarFileW, err := os.Create(tarFile) + require.NoError(t, err) + defer tarFileW.Close() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterTar, + Output: fixedWriteCloser(tarFileW), + }, + }, + }, nil) + require.NoError(t, err) + tarFileW.Close() + + tarFileR, err := os.Open(tarFile) + require.NoError(t, err) + defer tarFileR.Close() + tarR := tar.NewReader(tarFileR) + touched := map[string]*tar.Header{ + "bin/": nil, // Regular dir + "etc/": nil, // Parent of file mounts (etc/{resolv.conf, hosts}) + "var/": nil, // Parent of dir mount (var/foo/) + "tmp/": nil, // Grandparent of dir mount (tmp/foo2/bar/) + // No support for reproducing the timestamps of mount point directories such as var/foo/ and tmp/foo2/bar/, + // because the touched timestamp value is lost when the mount is unmounted. + } + for { + hd, err := tarR.Next() + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + if x, ok := touched[hd.Name]; ok && x == nil { + touched[hd.Name] = hd + } + } + for name, hd := range touched { + t.Logf("Verifying %q (%+v)", name, hd) + require.NotNil(t, hd, name) + require.Equal(t, sourceDateEpoch, hd.ModTime.Unix(), name) + } +} + +// #319 +func testMountWithNoSource(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)) + + var nilState llb.State + + // This should never actually be run, but we want to succeed + // if it was, because we expect an error below, or a daemon + // panic if the issue has regressed. + cmdArgs := integration.UnixOrWindows( + []string{"/bin/true"}, + []string{"cmd", "/C", "exit 0"}, + ) + run := busybox.Run( + llb.Args(cmdArgs), + llb.AddMount("/nil", nilState, llb.SourcePath("/"), llb.Readonly)) + + st = run.AddMount("/mnt", st) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + checkAllReleasable(t, c, sb, true) +} + +func testRawSocketMount(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + dir := t.TempDir() + sockPath := filepath.Join(dir, "test.sock") + listener := net.ListenConfig{} + l, err := listener.Listen(context.TODO(), "unix", sockPath) + require.NoError(t, err) + defer l.Close() + + var called atomic.Bool + srv := &http.Server{ + ReadHeaderTimeout: 10 * time.Second, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called.Store(true) + w.WriteHeader(http.StatusOK) + }), + } + go srv.Serve(l) + defer srv.Close() + + st := llb.Image("alpine:latest"). + Run(llb.Shlex(`apk add --no-cache curl`)). + Run(llb.Shlex(`curl --unix-socket /tmp/test.sock http://./foo`), + llb.AddSSHSocket(llb.SSHSocketTarget("/tmp/test.sock")), + ).Root() + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + raw, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ + Paths: []string{sockPath}, + Raw: true, + }}) + require.NoError(t, err) + + ch := make(chan *SolveStatus) + go func() { + for status := range ch { + for _, l := range status.Logs { + t.Log(string(l.Data)) + } + } + }() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Session: []session.Attachable{raw}, + }, ch) + require.NoError(t, err) + require.True(t, called.Load(), "server should have been called") +} + +// #324 +func testReadonlyRootFS(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("docker.io/library/busybox:latest") + st := llb.Scratch() + + // The path /foo should be unwriteable. + run := busybox.Run( + llb.ReadonlyRootFS(), + llb.Args([]string{"/bin/touch", "/foo"})) + st = run.AddMount("/mnt", st) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.Error(t, err) + // Would prefer to detect more specifically "Read-only file + // system" but that isn't exposed here (it is on the stdio + // which we don't see). + require.Contains(t, err.Error(), "process \"/bin/touch /foo\" did not complete successfully") + + checkAllReleasable(t, c, sb, true) +} + +func testRunCacheWithMounts(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) + + imgAlpine := integration.UnixOrWindows("alpine:latest", "nanoserver:plus") + alpineImage := llb.Image(imgAlpine) + + // On Windows ARM64, nanoserver:plus and nanoserver:latest map to the same + // image, so we create a marker file to distinguish the mounted image. + mountSource := alpineImage + if runtime.GOOS == "windows" { + mountSource = alpineImage.Run( + llb.Shlex(`cmd /C echo 1> C:/marker`), + ).Root() + } + + cmdStr := integration.UnixOrWindows( + `sh -e -c "[[ -f /m1/sbin/apk ]]"`, + `cmd /C if exist C:/m1/marker (exit 0) else (exit 1)`, + ) + out := busybox.Run(llb.Shlex(cmdStr)) + out.AddMount("/m1", mountSource, llb.Readonly) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + cmdStr = integration.UnixOrWindows( + `sh -e -c "[[ ! -f /m1/sbin/apk ]]"`, + `cmd /C if exist C:/m1/marker (exit 1) else (exit 0)`, + ) + out = busybox.Run(llb.Shlex(cmdStr)) + out.AddMount("/m1", llb.Image(imgName), llb.Readonly) + + def, err = out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +/* +testSecretEnv verifies that secrets can be injected as environment variables during a build. +It tests four scenarios: (1) a provided secret is accessible via env var, (2) an optional +secret that is not provided results in an unset/empty env var, (3) a required secret that +is not provided causes an error, and (4) multiple secrets with custom IDs are resolved correctly. +Works on both Linux (busybox) and Windows (nanoserver). +*/ +func testSecretEnv(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") + var st llb.ExecState + + // Test 1: Verify secret value is accessible as environment variable + switch imgName { + case "nanoserver:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`cmd /C "if "%MY_SECRET%"=="foo-secret" (exit 0) else (exit 1)"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) + case "busybox:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`sh -c '[ "$(echo ${MY_SECRET})" = 'foo-secret' ]'`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) + } + + 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{ + "MY_SECRET": []byte("foo-secret"), + })}, + }, nil) + require.NoError(t, err) + + // Test 2: Optional secret not provided should be unset/empty + switch imgName { + case "nanoserver:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`cmd /C "if not defined MY_SECRET (exit 0) else (exit 1)"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true), llb.SecretOptional)) + case "busybox:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`sh -c '[ -z "${MY_SECRET}" ]'`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true), llb.SecretOptional)) + } + + 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{})}, + }, nil) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + // Test 3: Required secret not provided should error + switch imgName { + case "nanoserver:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`cmd /C "echo foo"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) + case "busybox:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`echo foo`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) + } + + 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{})}, + }, nil) + require.Error(t, err) + + // Test 4: Multiple secrets with custom IDs + switch imgName { + case "nanoserver:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`cmd /C "if "%MYPASSWORD%-%MYTOKEN%"=="pw-token" (exit 0) else (exit 1)"`), + llb.AddSecret("MYPASSWORD", llb.SecretID("pass"), llb.SecretAsEnv(true)), + llb.AddSecret("MYTOKEN", llb.SecretAsEnv(true)), + ) + case "busybox:latest": + st = llb.Image(imgName). + Run(llb.Shlex(`sh -c '[ "$(echo ${MYPASSWORD}-${MYTOKEN})" = "pw-token" ]' `), + llb.AddSecret("MYPASSWORD", llb.SecretID("pass"), llb.SecretAsEnv(true)), + llb.AddSecret("MYTOKEN", llb.SecretAsEnv(true)), + ) + } + + 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{ + "pass": []byte("pw"), + "MYTOKEN": []byte("token"), + })}, + }, nil) + require.NoError(t, err) +} + +/* +testSecretMounts verifies file-based secret mounts: content readability, optional/required +behavior, custom permissions, and empty secrets. Skipped on Windows because BuildKit uses +tmpfs for secret mounts, which Windows does not support. See testSecretEnv for a +cross-platform alternative using environment-based secrets. +*/ +func testSecretMounts(t *testing.T, sb integration.Sandbox) { + // Windows vs Linux secret implementation differences: + // + // Linux: Secrets are mounted as files using tmpfs at /run/secrets/ (RAM-based, encrypted) + // + // Windows: Secrets are stored in C:\ProgramData\Docker\internal\secrets (clear text on disk) + // Symbolic links point to the desired target (default: C:\ProgramData\Docker\secrets) + // Windows does NOT support tmpfs or non-directory file bind-mounts + // UID/GID/mode options are NOT supported on Windows + // Recommend BitLocker for at-rest encryption + // + // BuildKit Issue: The "invalid windows mount type: 'tmpfs'" error occurs because BuildKit + // currently tries to use tmpfs for all secret mounts. This needs to be fixed in BuildKit's + // secret mount implementation to use the Windows symlink approach instead. + + // For now, this test is Linux-only until BuildKit properly implements Windows secret mounts + // without tmpfs. Use testSecretEnv for Windows-compatible environment-based secrets. + integration.SkipOnPlatform(t, "windows", "Windows does not support tmpfs for secret mounts") + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + // Test 1: Basic secret mount with content verification (Linux only) + st := llb.Image("busybox:latest"). + Run(llb.Shlex(`sh -c 'mount | grep mysecret | grep "type tmpfs" && [ "$(cat /run/secrets/mysecret)" = 'foo-secret' ]'`), llb.AddSecret("/run/secrets/mysecret")) + + 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{ + "/run/secrets/mysecret": []byte("foo-secret"), + })}, + }, nil) + require.NoError(t, err) + + // Test 2: Optional secret - mount should not exist when secret not present in SolveOpt + st = llb.Image("busybox:latest"). + Run(llb.Shlex(`test ! -f /run/secrets/mysecret2`), llb.AddSecret("/run/secrets/mysecret2", llb.SecretOptional)) + + 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{})}, + }, nil) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) + + // Test 3: Required secret missing - should error + st = llb.Image("busybox:latest"). + Run(llb.Shlex(`echo secret3`), llb.AddSecret("/run/secrets/mysecret3")) + + 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{})}, + }, nil) + require.Error(t, err) + + // Test 4: Secret with custom ID and file permissions + st = llb.Image("busybox:latest"). + Run(llb.Shlex(`sh -c '[ "$(stat -c "%u %g %f" /run/secrets/mysecret4)" = "1 1 81ff" ]' `), llb.AddSecret("/run/secrets/mysecret4", llb.SecretID("mysecret"), llb.SecretFileOpt(1, 1, 0777))) + + 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{ + "mysecret": []byte("pw"), + })}, + }, nil) + require.NoError(t, err) + + // Test 5: Empty secret still creates secret file + st = llb.Image("busybox:latest"). + Run(llb.Shlex(`test -f /run/secrets/mysecret5`), llb.AddSecret("/run/secrets/mysecret5", llb.SecretID("mysecret"))) + + 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{ + "mysecret": []byte(""), + })}, + }, nil) + require.NoError(t, err) +} + +func testSharedCacheMounts(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; while [[ ! -f two ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) + st.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + + st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; while [[ ! -f one ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) + st2.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + + out := busybox.Run(llb.Shlex("true")) + out.AddMount("/m1", st.Root()) + out.AddMount("/m2", st2.Root()) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +// #2334 +func testSharedCacheMountsNoScratch(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; while [[ ! -f two ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) + st.AddMount("/wd", llb.Image("busybox:latest"), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + + st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; while [[ ! -f one ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) + st2.AddMount("/wd", llb.Image("busybox:latest"), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) + + out := busybox.Run(llb.Shlex("true")) + out.AddMount("/m1", st.Root()) + out.AddMount("/m2", st2.Root()) + + def, err := out.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func testSSHMount(t *testing.T, sb integration.Sandbox) { + integration.SkipOnPlatform(t, "windows") + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + a := agent.NewKeyring() + + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + err = a.Add(agent.AddedKey{PrivateKey: k}) + require.NoError(t, err) + + sockPath, err := makeSSHAgentSock(t, a) + require.NoError(t, err) + + ssh, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ + Paths: []string{sockPath}, + }}) + require.NoError(t, err) + + // no ssh exposed + st := llb.Image("busybox:latest").Run(llb.Shlex(`nosuchcmd`), llb.AddSSHSocket()) + 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(), "no SSH key ") + + // custom ID not exposed + st = llb.Image("busybox:latest").Run(llb.Shlex(`nosuchcmd`), llb.AddSSHSocket(llb.SSHID("customID"))) + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Session: []session.Attachable{ssh}, + }, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "unset ssh forward key customID") + + // missing custom ID ignored on optional + st = llb.Image("busybox:latest").Run(llb.Shlex(`ls`), llb.AddSSHSocket(llb.SSHID("customID"), llb.SSHOptional)) + def, err = st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Session: []session.Attachable{ssh}, + }, nil) + require.NoError(t, err) + + // valid socket + st = llb.Image("alpine:latest"). + Run(llb.Shlex(`apk add --no-cache openssh`)). + Run(llb.Shlex(`sh -c 'echo -n $SSH_AUTH_SOCK > /out/sock && ssh-add -l > /out/out'`), + llb.AddSSHSocket()) + + 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, + }, + }, + Session: []session.Attachable{ssh}, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "sock")) + require.NoError(t, err) + require.Equal(t, "/run/buildkit/ssh_agent.0", string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "out")) + require.NoError(t, err) + require.Contains(t, string(dt), "2048") + require.Contains(t, string(dt), "(RSA)") + + // forbidden command + st = llb.Image("alpine:latest"). + Run(llb.Shlex(`apk add --no-cache openssh`)). + Run(llb.Shlex(`sh -c 'ssh-keygen -f /tmp/key -N "" && ssh-add -k /tmp/key 2> /out/out || true'`), + llb.AddSSHSocket()) + + out = st.AddMount("/out", llb.Scratch()) + def, err = out.Marshal(sb.Context()) + require.NoError(t, err) + + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + Session: []session.Attachable{ssh}, + }, nil) + require.NoError(t, err) + + dt, err = os.ReadFile(filepath.Join(destDir, "out")) + require.NoError(t, err) + require.Contains(t, string(dt), "agent refused operation") + + // valid socket from key on disk + st = llb.Image("alpine:latest"). + Run(llb.Shlex(`apk add --no-cache openssh`)). + Run(llb.Shlex(`sh -c 'ssh-add -l > /out/out'`), + llb.AddSSHSocket()) + + out = st.AddMount("/out", llb.Scratch()) + def, err = out.Marshal(sb.Context()) + require.NoError(t, err) + + k, err = rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + dt = pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + }, + ) + + tmpDir := t.TempDir() + + err = os.WriteFile(filepath.Join(tmpDir, "key"), dt, 0600) + require.NoError(t, err) + + ssh, err = sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ + Paths: []string{filepath.Join(tmpDir, "key")}, + }}) + require.NoError(t, err) + + destDir = t.TempDir() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + Session: []session.Attachable{ssh}, + }, nil) + require.NoError(t, err) + + dt, err = os.ReadFile(filepath.Join(destDir, "out")) + require.NoError(t, err) + require.Contains(t, string(dt), "2048") + require.Contains(t, string(dt), "(RSA)") +} + +func testTmpfsMounts(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 'mount | grep /foobar | grep "type tmpfs" && touch /foobar/test'`), llb.AddMount("/foobar", llb.Scratch(), llb.Tmpfs())) + + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) + require.NoError(t, err) +} + +func cacheMountFile(base llb.State, cacheID, name string) llb.State { + check := base.Run(llb.Shlexf(`sh -e -c "echo %s >/dev/null; cp /cache/%s /out/%s"`, cacheID, name, name)) + check.AddMount("/cache", llb.Scratch(), llb.AsPersistentCacheDir(cacheID, llb.CacheMountShared)) + return check.AddMount("/out", llb.Scratch()) +} + +func makeSSHAgentSock(t *testing.T, agent agent.Agent) (p string, err error) { + tmpDir := integration.Tmpdir(t) + sockPath := filepath.Join(tmpDir.Name, "ssh_auth_sock") + + listener := net.ListenConfig{} + l, err := listener.Listen(context.TODO(), "unix", sockPath) + if err != nil { + return "", err + } + t.Cleanup(func() { + require.NoError(t, l.Close()) + }) + + s := &server{l: l} + go s.run(agent) + + return sockPath, nil +} + +type server struct { + l net.Listener +} + +func (s *server) run(a agent.Agent) error { + for { + c, err := s.l.Accept() + if err != nil { + return err + } + + go agent.ServeAgent(a, c) + } +} diff --git a/client/client_network_test.go b/client/client_network_test.go new file mode 100644 index 000000000..671e6bc1f --- /dev/null +++ b/client/client_network_test.go @@ -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{} +) diff --git a/client/client_oci_source_test.go b/client/client_oci_source_test.go new file mode 100644 index 000000000..5714abc91 --- /dev/null +++ b/client/client_oci_source_test.go @@ -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) +} diff --git a/client/client_session_test.go b/client/client_session_test.go new file mode 100644 index 000000000..9dfa96a71 --- /dev/null +++ b/client/client_session_test.go @@ -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 +} diff --git a/client/client_source_map_test.go b/client/client_source_map_test.go new file mode 100644 index 000000000..fac51e235 --- /dev/null +++ b/client/client_source_map_test.go @@ -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) +} diff --git a/client/client_test.go b/client/client_test.go index d66046ac3..3a18a318c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,98 +1,10 @@ package client import ( - "archive/tar" - "bytes" - "compress/gzip" - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/binary" - "encoding/json" - "encoding/pem" - "fmt" - "io" - "net" - "net/http" - "net/http/httptest" - "net/url" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "syscall" "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/images" - "github.com/containerd/containerd/v2/core/remotes/docker" - "github.com/containerd/containerd/v2/core/snapshots" - "github.com/containerd/containerd/v2/pkg/namespaces" - "github.com/containerd/containerd/v2/plugins/content/local" - "github.com/containerd/continuity/fs/fstest" - cerrdefs "github.com/containerd/errdefs" - "github.com/containerd/platforms" - "github.com/distribution/reference" - intoto "github.com/in-toto/in-toto-golang/in_toto" - controlapi "github.com/moby/buildkit/api/services/control" - cacheimporttypes "github.com/moby/buildkit/cache/remotecache/v1/types" - "github.com/moby/buildkit/client/llb" - "github.com/moby/buildkit/client/llb/sourceresolver" - "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" - sessionsecrets "github.com/moby/buildkit/session/secrets" - "github.com/moby/buildkit/session/secrets/secretsprovider" - "github.com/moby/buildkit/session/sshforward/sshprovider" - "github.com/moby/buildkit/solver/errdefs" - provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types" - "github.com/moby/buildkit/solver/pb" - "github.com/moby/buildkit/solver/result" - "github.com/moby/buildkit/sourcepolicy" - sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb" - "github.com/moby/buildkit/util/attestation" - "github.com/moby/buildkit/util/contentutil" - "github.com/moby/buildkit/util/entitlements" - "github.com/moby/buildkit/util/gitutil/gitobject" - "github.com/moby/buildkit/util/iohelper" - "github.com/moby/buildkit/util/purl" - "github.com/moby/buildkit/util/testutil" - containerdutil "github.com/moby/buildkit/util/testutil/containerd" - "github.com/moby/buildkit/util/testutil/echoserver" - "github.com/moby/buildkit/util/testutil/helpers" - "github.com/moby/buildkit/util/testutil/httpserver" "github.com/moby/buildkit/util/testutil/integration" "github.com/moby/buildkit/util/testutil/workers" - policyimage "github.com/moby/policy-helpers/image" - digest "github.com/opencontainers/go-digest" - ocispecs "github.com/opencontainers/image-spec/specs-go/v1" - packageurl "github.com/package-url/packageurl-go" - "github.com/pkg/errors" - "github.com/spdx/tools-golang/spdx" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tonistiigi/fsutil" - fsutiltypes "github.com/tonistiigi/fsutil/types" - "golang.org/x/crypto/ssh/agent" - "golang.org/x/mod/semver" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" ) func init() { @@ -105,189 +17,296 @@ func init() { } var allTests = []func(t *testing.T, sb integration.Sandbox){ - testCacheExportCacheKeyLoop, - testRelativeWorkDir, - testFileOpMkdirMkfile, - testFileOpCopyRm, - testFileOpCopyIncludeExclude, - testFileOpCopyAlwaysReplaceExistingDestPaths, - testFileOpRmWildcard, - testFileOpCopyUIDCache, - testFileOpCopyChmodText, - testCallDiskUsage, - testBuildMultiMount, - testBuildHTTPSource, - testBuildHTTPSourceUnauthorizedChecksumRace, - testBuildHTTPSourceEtagScope, - testBuildHTTPSourceAuthHeaderSecret, - testBuildHTTPSourceHostTokenSecret, - testBuildHTTPSourceHeader, - testBuildHTTPSourcePGPSignatureVerify, - testBuildPushAndValidate, - testBuildExportWithUncompressed, - testBuildExportScratch, - testResolveAndHosts, - testUser, - testOCIExporter, - testOCIExporterContentStore, - testSessionExporter, - testWhiteoutParentDir, - testFrontendImageNaming, - testDuplicateWhiteouts, - testMountWithNoSource, - testInvalidExporter, - testReadonlyRootFS, - testBasicRegistryCacheImportExport, - testBasicLocalCacheImportExport, - testBasicS3CacheImportExport, + // client_cache_test.go testBasicAzblobCacheImportExport, - testCachedMounts, - testCopyFromEmptyImage, - testProxyEnv, - testLocalSymlinkEscape, - testTmpfsMounts, - testSharedCacheMounts, - testSharedCacheMountsNoScratch, - testLockedCacheMounts, - testDuplicateCacheMount, - testRunCacheWithMounts, - testParallelLocalBuilds, - testSecretEnv, - testSecretMounts, - testSessionHealthMonitorFailsBlockedSecretResponse, - testExtraHosts, - testShmSize, - testUlimit, - testCgroupParent, - testLinuxResources, - testNetworkMode, - testFrontendMetadataReturn, - testFrontendUseSolveResults, - testSSHMount, - testRawSocketMount, - testStdinClosed, - testHostnameLookup, - testHostnameSpecifying, - testPushByDigest, - testPushProgressSameVertex, - testPullWithDigestCheck, testBasicInlineCacheImportExport, - testExportBusyboxLocal, - testBridgeNetworking, - testCacheMountNoCache, - testExporterTargetExists, - testTarExporterWithSocket, - testTarExporterWithSocketCopy, - testTarExporterSymlink, - testMultipleRegistryCacheImportExport, - testMultipleExporters, - testSourceMap, - testSourceMapFromRef, - testLazyImagePush, - testStargzLazyPull, - testStargzLazyInlineCacheImportExport, - testFileOpInputSwap, - testRelativeMountpoint, - testLocalSourceDiffer, - testLocalSourceWithHardlinksFilter, - testNoTarOCIIndexMediaType, - testOCILayoutSource, - testOCILayoutPlatformSource, - testBuildExportZstd, - testPullZstdImage, - testMergeOp, - testPassthroughOp, - testMergeOpCacheInline, - testMergeOpCacheMin, - testMergeOpCacheMax, - testRmSymlink, - testMoveParentDir, - testBuildExportWithForeignLayer, - testZstdLocalCacheExport, - testCacheExportIgnoreError, + testBasicLocalCacheImportExport, + testBasicRegistryCacheImportExport, + testBasicS3CacheImportExport, testCacheExportCacheDeletedContent, + testCacheExportCacheKeyLoop, + testCacheExportIgnoreError, + testImageManifestRegistryCacheImportExport, testLocalCacheExportReset, - testZstdRegistryCacheImportExport, - testZstdLocalCacheImportExport, + testMultipleCacheExports, + testMultipleRecordsWithSameLayersCacheImportExport, + testMultipleRegistryCacheImportExport, + testRegistryEmptyCacheExport, + testSnapshotWithMultipleBlobs, testUncompressedLocalCacheImportExport, testUncompressedRegistryCacheImportExport, + testZstdLocalCacheExport, + testZstdLocalCacheImportExport, + testZstdRegistryCacheImportExport, + testStargzLazyInlineCacheImportExport, testStargzLazyRegistryCacheImportExport, - testValidateDigestOrigin, + + // client_control_test.go testCallInfo, - testPullWithLayerLimit, + testClientCustomGRPCOpts, + testListenBuildHistoryExcludesSoftDeletedRecords, + + // client_diskusage_test.go + testCallDiskUsage, + + // client_exec_test.go + testCgroupParent, + testLinuxResources, + testPassthroughOp, + testRelativeMountpoint, + testRelativeWorkDir, + testRunValidExitCodes, + testShmSize, + testStdinClosed, + testUlimit, + testUser, + + // client_export_image_test.go + testBuildExportScratch, + testBuildExportWithForeignLayer, + testBuildExportWithUncompressed, + testBuildExportZstd, + testBuildPushAndValidate, + testExportBusyboxLocal, + testExportedImageLabels, + testImageResolveConfigDefaultLocalFallback, + testInvalidExporter, + testLazyImagePush, + testOCIExporter, + testOCIExporterContentStore, + testPullWithDigestCheck, + testPullZstdImage, + testPushByDigest, + testPushProgressSameVertex, + testStargzLazyPull, + + // client_export_local_test.go + testExportLocalForcePlatformSplit, + testExportLocalModeCopyKeepsStaleDestinationFiles, + testExportLocalModeCopyMultiPlatformKeepsAllPlatforms, + testExportLocalModeDeleteRemovesStaleDestinationFiles, + testExportLocalModeDeleteMultiPlatformKeepsAllPlatforms, + testExportLocalModeInvalid, + testExportLocalNoPlatformSplit, + testExportLocalNoPlatformSplitOverwrite, + testExportTarPlatformIDSanitized, + testExporterTargetExists, + testMultipleExporters, + testSessionExporter, + testTarExporterSymlink, + testTarExporterWithSocket, + testTarExporterWithSocketCopy, + + // client_export_metadata_test.go + testAttestationBundle, + testAttestationDefaultSubject, testExportAnnotations, testExportAnnotationsMediaTypes, testExportAttestationsDefaultOCIArtifact, - testExportAttestationsOCIArtifact, testExportAttestationsImageManifest, - testExportedImageLabels, - testAttestationDefaultSubject, - testSourceDateEpochLayerTimestamps, - testSourceDateEpochClamp, - testSourceDateEpochReset, - testSourceDateEpochLocalExporter, - testSourceDateEpochTarExporter, - testSourceDateEpochImageExporter, - testAttestationBundle, + testExportAttestationsOCIArtifact, + testImageResolveAttestationChainLocal, + testImageResolveAttestationChainRequiresNetwork, + testImageResolveProvenanceAttestation, testSBOMScan, testSBOMScanSingleRef, testSBOMSupplements, - testMultipleCacheExports, - testMountStubsDirectory, - testMountStubsTimestamp, - testSourcePolicy, - testImageManifestRegistryCacheImportExport, - testLLBMountPerformance, - testClientCustomGRPCOpts, - testMultipleRecordsWithSameLayersCacheImportExport, - testRegistryEmptyCacheExport, - testSnapshotWithMultipleBlobs, - testImageBlobSource, - testOCILayoutBlobSource, - testExportLocalNoPlatformSplit, - testExportLocalNoPlatformSplitOverwrite, - testExportLocalForcePlatformSplit, - testExportTarPlatformIDSanitized, - testExportLocalModeCopyKeepsStaleDestinationFiles, - testExportLocalModeDeleteRemovesStaleDestinationFiles, - testExportLocalModeCopyMultiPlatformKeepsAllPlatforms, - testExportLocalModeDeleteMultiPlatformKeepsAllPlatforms, - testExportLocalModeInvalid, - testOCIIndexMediatype, - testLayerLimitOnMounts, - testFrontendVerifyPlatforms, - testFrontendLintSkipVerifyPlatforms, - testRunValidExitCodes, + testSourceDateEpochClamp, + testSourceDateEpochImageExporter, + testSourceDateEpochLayerTimestamps, + testSourceDateEpochLocalExporter, + testSourceDateEpochReset, + testSourceDateEpochTarExporter, + + // client_fileop_test.go + testCopyFromEmptyImage, + testDuplicateWhiteouts, + testFileOpCopyAlwaysReplaceExistingDestPaths, + testFileOpCopyChmodText, + testFileOpCopyIncludeExclude, + testFileOpCopyRm, + testFileOpCopyUIDCache, + testFileOpInputSwap, + testFileOpMkdirMkfile, + testFileOpRmWildcard, testFileOpSymlink, - testMetadataOnlyLocal, + testMoveParentDir, + testRmSymlink, + testWhiteoutParentDir, + + // client_frontend_test.go + testFrontendImageNaming, + testFrontendLintSkipVerifyPlatforms, + testFrontendMetadataReturn, + testFrontendUseSolveResults, + testFrontendVerifyPlatforms, + + // client_git_source_test.go + testGitBundleRoundTrip, + testGitBundleRoundTripRegistry, + testGitResolveMutatedSource, testGitResolveSourceMetadata, - testHTTPResolveSourceMetadata, + + // client_http_source_test.go + testBuildHTTPSource, + testBuildHTTPSourceAuthHeaderSecret, + testBuildHTTPSourceEtagScope, + testBuildHTTPSourceHeader, + testBuildHTTPSourceHostTokenSecret, + testBuildHTTPSourcePGPSignatureVerify, + testBuildHTTPSourceUnauthorizedChecksumRace, testHTTPPruneAfterCacheKey, testHTTPPruneAfterResolveMeta, testHTTPResolveMetaReuse, testHTTPResolveMultiBuild, - testImageResolveConfigDefaultLocalFallback, - testGitResolveMutatedSource, - testGitBundleRoundTrip, - testGitBundleRoundTripRegistry, - testImageResolveAttestationChainRequiresNetwork, - testImageResolveAttestationChainLocal, - testImageResolveProvenanceAttestation, + testHTTPResolveSourceMetadata, + + // client_image_source_test.go + testClientGatewayCanceledCredentialsCallbackReturns, + testPullWithLayerLimit, + testValidateDigestOrigin, + + // client_local_source_test.go + testLocalSourceDiffer, + testLocalSourceWithHardlinksFilter, + testLocalSymlinkEscape, + testMetadataOnlyLocal, + testParallelLocalBuilds, + + // client_mergeop_test.go + testMergeOp, + testMergeOpCacheInline, + testMergeOpCacheMax, + testMergeOpCacheMin, + + // client_mount_test.go + testBuildMultiMount, + testCachedMounts, + testCacheMountNoCache, + testDuplicateCacheMount, + testLayerLimitOnMounts, + testLLBMountPerformance, + testLockedCacheMounts, + testMountStubsDirectory, + testMountStubsTimestamp, + testMountWithNoSource, + testRawSocketMount, + testReadonlyRootFS, + testRunCacheWithMounts, + testSecretEnv, + testSecretMounts, + testSharedCacheMounts, + testSharedCacheMountsNoScratch, + testSSHMount, + testTmpfsMounts, + + // client_network_test.go + testBridgeNetworking, + testExtraHosts, + testHostnameLookup, + testHostnameSpecifying, + testNetworkMode, + testProxyEnv, + testResolveAndHosts, + + // client_oci_source_test.go + testImageBlobSource, + testNoTarOCIIndexMediaType, + testOCIIndexMediatype, + testOCILayoutBlobSource, + testOCILayoutPlatformSource, + testOCILayoutSource, + + // client_session_test.go + testSessionHealthMonitorFailsBlockedSecretResponse, + + // client_source_map_test.go + testSourceMap, + testSourceMapFromRef, + + // client_warnings_test.go + testWarnings, + + // policy_test.go testSourcePolicySession, testSourcePolicySessionDenyMessages, testSourceMetaPolicySession, testSourceMetaPolicySessionResolveAttestations, testSourcePolicyParallelSession, testSourcePolicySignedCommit, - testSourcePolicySessionHTTPChecksumAssist, testSourcePolicySessionConvert, - testListenBuildHistoryExcludesSoftDeletedRecords, + testSourcePolicySessionHTTPChecksumAssist, + testSourcePolicy, } func TestIntegration(t *testing.T) { testIntegration(t, append(allTests, validationTests...)...) } +func TestClientGatewayIntegration(t *testing.T) { + integration.Run(t, integration.TestFuncs( + // gateway_container_exec_test.go + testClientGatewayContainerCancelExecTty, + testClientGatewayContainerCancelOnRelease, + testClientGatewayContainerCancelPID1Tty, + testClientGatewayContainerExecPipe, + testClientGatewayContainerExecPipeRelease, + testClientGatewayContainerExecPipeSignalKill, + testClientGatewayContainerExecTty, + testClientGatewayContainerPID1Exit, + testClientGatewayContainerPID1Fail, + testClientGatewayContainerPID1Tty, + testClientGatewayContainerSignal, + testClientGatewayExecError, + testClientGatewayExecFileActionError, + testClientGatewaySlowCacheExecError, + + // gateway_container_mount_test.go + testClientGatewayContainerMounts, + testClientGatewayContainerPlatformPATH, + testClientGatewayContainerSecretEnv, + + // gateway_container_network_test.go + testClientGatewayContainerExtraHosts, + + // gateway_container_security_test.go + testClientGatewayContainerInvalidSecurityMode, + + // gateway_solve_test.go + testClientGatewayEmptyImageExec, + testClientGatewayEmptySolve, + testClientGatewayFailedSolve, + testClientGatewayNilResult, + testClientGatewaySolve, + testClientSlowCacheRootfsRef, + testNoBuildID, + testUnknownBuildID, + ), integration.WithMirroredImages(integration.OfficialImages("busybox:latest"))) + + integration.Run(t, integration.TestFuncs( + // gateway_container_security_test.go + testClientGatewayContainerSecurityModeCaps, + testClientGatewayContainerSecurityModeValidation, + ), integration.WithMirroredImages(integration.OfficialImages("busybox:latest")), + integration.WithMatrix("secmode", map[string]any{ + "sandbox": securitySandbox, + "insecure": securityInsecure, + }), + ) + + integration.Run(t, integration.TestFuncs( + // gateway_container_network_test.go + testClientGatewayContainerHostNetworkingAccess, + testClientGatewayContainerHostNetworkingValidation, + ), + integration.WithMirroredImages(integration.OfficialImages("busybox:latest")), + integration.WithMatrix("netmode", map[string]any{ + "default": defaultNetwork, + "host": hostNetwork, + }), + ) +} + func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sandbox)) { mirroredImagesUnix := integration.OfficialImages("busybox:latest", "alpine:latest") mirroredImagesUnix["tonistiigi/test:nolayers"] = "docker.io/tonistiigi/test:nolayers" @@ -305,6 +324,7 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa integration.SkipOnPlatform(t, "windows") integration.Run(t, integration.TestFuncs( + // client_exec_test.go testSecurityMode, testSecurityModeSysfs, testSecurityModeErrors, @@ -317,6 +337,7 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa ) integration.Run(t, integration.TestFuncs( + // client_network_test.go testHostNetworking, ), mirrors, @@ -327,6 +348,7 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa ) integration.Run(t, integration.TestFuncs( + // policy_test.go testProxyNetworkNoRootless, testProxyNetworkModesNoRootless, testProxyNetworkDefaultEgressNoRootless, @@ -342,14711 +364,16 @@ func testIntegration(t *testing.T, funcs ...func(t *testing.T, sb integration.Sa integration.Run( t, - integration.TestFuncs(testBridgeNetworkingDNSNoRootless), + integration.TestFuncs( + // client_network_test.go + testBridgeNetworkingDNSNoRootless, + ), mirrors, integration.WithMatrix("netmode", map[string]any{ "dns": bridgeDNSNetwork, }), ) + // client_cdi_test.go integration.Run(t, integration.TestFuncs(cdiTests...), mirrors) } - -func newContainerd(cdAddress string) (*ctd.Client, error) { - return ctd.New(cdAddress, ctd.WithTimeout(60*time.Second)) -} - -// moby/buildkit#1336 -func testCacheExportCacheKeyLoop(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - tmpdir := integration.Tmpdir(t) - - err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600) - require.NoError(t, err) - - imgName := integration.UnixOrWindows("alpine:latest", "nanoserver:latest") - scratch := integration.UnixOrWindows( - llb.Scratch(), - llb.Image(imgName), - ) - cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0") - for _, mode := range []bool{false, true} { - func(mode bool) { - t.Run(fmt.Sprintf("mode=%v", mode), func(t *testing.T) { - buildbase := llb.Image(imgName).File(llb.Copy(llb.Local("mylocal"), "foo", "foo")) - if mode { // same cache keys with a separating node go to different code-path - buildbase = buildbase.Run(llb.Shlex(cmdStr)).Root() - } - intermed := llb.Image(imgName).File(llb.Copy(buildbase, "foo", "foo")) - final := scratch.File(llb.Copy(intermed, "foo", "foooooo")) - - def, err := final.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": filepath.Join(tmpdir.Name, "cache"), - }, - }, - }, - LocalMounts: map[string]fsutil.FS{ - "mylocal": tmpdir, - }, - }, nil) - require.NoError(t, err) - }) - }(mode) - } -} - -func testCacheExportCacheDeletedContent(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - tmpdir := integration.Tmpdir(t) - - err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600) - require.NoError(t, err) - - base := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo abc-def > /foo && echo abc > /detection-file"`)).Root() - st := llb.Scratch().File(llb.Copy(base, "/foo", "/out")) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": filepath.Join(tmpdir.Name, "cache"), - "mode": "max", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(filepath.Join(tmpdir.Name, "cache", "index.json")) - require.NoError(t, err) - - var index ocispecs.Index - err = json.Unmarshal(dt, &index) - require.NoError(t, err) - - require.Len(t, index.Manifests, 1) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", index.Manifests[0].Digest.Hex())) - require.NoError(t, err) - - var mfst ocispecs.Manifest - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - - require.Len(t, mfst.Layers, 3) - require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst.Config.MediaType) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", mfst.Config.Digest.Hex())) - require.NoError(t, err) - - var cc cacheimporttypes.CacheConfig - err = json.Unmarshal(dt, &cc) - require.NoError(t, err) - - require.Equal(t, 3, len(cc.Layers)) - require.Equal(t, 5, len(cc.Records)) - - var runLayer *int - for i, l := range cc.Layers { - if l.ParentIndex != -1 { - if runLayer != nil { - t.Fatal("multiple RUN layers") - } - runLayer = &i - } - } - require.NotNil(t, runLayer) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex())) - require.NoError(t, err) - - m, err := testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - require.Equal(t, "abc\n", string(m["detection-file"].Data)) - - // delete the blob for the run layer - err = os.Remove(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex())) - require.NoError(t, err) - - // prune all buildkit state - ensurePruneAll(t, c, sb) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - CacheImports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "src": filepath.Join(tmpdir.Name, "cache"), - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": filepath.Join(tmpdir.Name, "cache2"), - "mode": "max", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2", "index.json")) - require.NoError(t, err) - - var index2 ocispecs.Index - err = json.Unmarshal(dt, &index2) - require.NoError(t, err) - - require.Len(t, index2.Manifests, 1) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", index2.Manifests[0].Digest.Hex())) - require.NoError(t, err) - - var mfst2 ocispecs.Manifest - err = json.Unmarshal(dt, &mfst2) - require.NoError(t, err) - - require.Len(t, mfst2.Layers, 1) - require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst2.Config.MediaType) - - dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", mfst2.Config.Digest.Hex())) - require.NoError(t, err) - - var cc2 cacheimporttypes.CacheConfig - err = json.Unmarshal(dt, &cc2) - require.NoError(t, err) - - require.Equal(t, 1, len(cc2.Layers)) - require.Equal(t, 5, len(cc2.Records)) - - for i, r := range cc.Records { - require.Equal(t, cc2.Records[i].Digest, r.Digest) - require.Equal(t, cc2.Records[i].Inputs, r.Inputs) - } -} - -func testLocalCacheExportReset(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - tmpdir := integration.Tmpdir(t) - cacheDir := filepath.Join(tmpdir.Name, "cache") - - // Build 1: export cache for a simple build - st1 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build1 > /out"`)).Root() - def1, err := llb.Scratch().File(llb.Copy(st1, "/out", "/out")).Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def1, SolveOpt{ - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": cacheDir, - }, - }, - }, - }, nil) - require.NoError(t, err) - - // Verify blobs were written - blobDir := filepath.Join(cacheDir, "blobs", "sha256") - entries1, err := os.ReadDir(blobDir) - require.NoError(t, err) - require.Greater(t, len(entries1), 0) - - // Build 2: different build, export with reset=true - st2 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build2-different > /out2"`)).Root() - def2, err := llb.Scratch().File(llb.Copy(st2, "/out2", "/out2")).Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def2, SolveOpt{ - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": cacheDir, - "reset": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - // Read the manifest to determine which blobs should be referenced - dt, err := os.ReadFile(filepath.Join(cacheDir, "index.json")) - require.NoError(t, err) - - var index ocispecs.Index - err = json.Unmarshal(dt, &index) - require.NoError(t, err) - require.Len(t, index.Manifests, 1) - - dt, err = os.ReadFile(filepath.Join(blobDir, index.Manifests[0].Digest.Hex())) - require.NoError(t, err) - - var mfst ocispecs.Manifest - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - - // Build the set of expected blobs: manifest + config + all layers - expectedBlobs := map[string]struct{}{ - index.Manifests[0].Digest.Hex(): {}, - mfst.Config.Digest.Hex(): {}, - } - for _, l := range mfst.Layers { - expectedBlobs[l.Digest.Hex()] = struct{}{} - } - - // Verify only referenced blobs remain - entries2, err := os.ReadDir(blobDir) - require.NoError(t, err) - - actualBlobs := map[string]struct{}{} - for _, e := range entries2 { - actualBlobs[e.Name()] = struct{}{} - } - require.Equal(t, expectedBlobs, actualBlobs, "after reset, only blobs referenced by the current manifest should remain") -} - -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) -} - -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 testExportedImageLabels(t *testing.T, sb integration.Sandbox) { - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - cdAddress := sb.ContainerdAddress() - if cdAddress == "" { - t.Skip("only supported with containerd") - } - - ctx := sb.Context() - - imgName := integration.UnixOrWindows("busybox", "nanoserver") - prefix := integration.UnixOrWindows( - "", - "cmd /C ", // TODO(profnandaa): currently needs the shell prefix, to be fixed - ) - def, err := llb.Image(imgName). - Run(llb.Shlexf(fmt.Sprintf("%secho foo > /foo", prefix))). - Marshal(ctx) - require.NoError(t, err) - - target := "docker.io/buildkit/build/exporter:labels" - - _, err = c.Solve(ctx, def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - }, - }, - }, - }, nil) - require.NoError(t, err) - - client, err := newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - - ctx = namespaces.WithNamespace(ctx, "buildkit") - - img, err := client.GetImage(ctx, target) - require.NoError(t, err) - - store := client.ContentStore() - - info, err := store.Info(ctx, img.Target().Digest) - require.NoError(t, err) - - dt, err := content.ReadBlob(ctx, store, img.Target()) - require.NoError(t, err) - - var mfst ocispecs.Manifest - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - - require.Equal(t, 2, len(mfst.Layers)) - - hasLabel := func(dgst digest.Digest) bool { - for k, v := range info.Labels { - if strings.HasPrefix(k, "containerd.io/gc.ref.content.") && v == dgst.String() { - return true - } - } - return false - } - - // check that labels are set on all layers and config - for _, l := range mfst.Layers { - require.True(t, hasLabel(l.Digest)) - } - require.True(t, hasLabel(mfst.Config.Digest)) - - err = c.Prune(sb.Context(), nil, PruneAll) - require.NoError(t, err) - - // layer should not be deleted - _, err = store.Info(ctx, mfst.Layers[1].Digest) - require.NoError(t, err) - - err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) - require.NoError(t, err) - - // layers should be deleted - _, err = store.Info(ctx, mfst.Layers[1].Digest) - require.Error(t, err) - require.True(t, errors.Is(err, cerrdefs.ErrNotFound)) - - // config should be deleted - _, err = store.Info(ctx, mfst.Config.Digest) - require.Error(t, err) - require.True(t, errors.Is(err, cerrdefs.ErrNotFound)) - - // buildkit contentstore still has the layer because it is multi-ns - bkstore := proxy.NewContentStore(c.ContentClient()) - - // layer should be deleted as not kept by history - _, err = bkstore.Info(ctx, mfst.Layers[1].Digest) - require.Error(t, err) - require.Contains(t, err.Error(), "not found") - - // config should still be there - _, err = bkstore.Info(ctx, img.Metadata().Target.Digest) - require.NoError(t, err) - - _, err = bkstore.Info(ctx, mfst.Config.Digest) - require.NoError(t, err) - - 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) - } - - // now everything should be deleted - _, err = bkstore.Info(ctx, img.Metadata().Target.Digest) - require.Error(t, err) - - _, err = bkstore.Info(ctx, mfst.Config.Digest) - require.Error(t, err) -} - -// #877 -func testExportBusyboxLocal(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - def, err := llb.Image("busybox").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) - - fi, err := os.Stat(filepath.Join(destDir, "bin/busybox")) - require.NoError(t, err) - - fi2, err := os.Stat(filepath.Join(destDir, "bin/vi")) - require.NoError(t, err) - - require.True(t, os.SameFile(fi, fi2)) -} - -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) -} - -// 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 testSSHMount(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - a := agent.NewKeyring() - - k, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - err = a.Add(agent.AddedKey{PrivateKey: k}) - require.NoError(t, err) - - sockPath, err := makeSSHAgentSock(t, a) - require.NoError(t, err) - - ssh, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ - Paths: []string{sockPath}, - }}) - require.NoError(t, err) - - // no ssh exposed - st := llb.Image("busybox:latest").Run(llb.Shlex(`nosuchcmd`), llb.AddSSHSocket()) - 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(), "no SSH key ") - - // custom ID not exposed - st = llb.Image("busybox:latest").Run(llb.Shlex(`nosuchcmd`), llb.AddSSHSocket(llb.SSHID("customID"))) - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Session: []session.Attachable{ssh}, - }, nil) - require.Error(t, err) - require.Contains(t, err.Error(), "unset ssh forward key customID") - - // missing custom ID ignored on optional - st = llb.Image("busybox:latest").Run(llb.Shlex(`ls`), llb.AddSSHSocket(llb.SSHID("customID"), llb.SSHOptional)) - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Session: []session.Attachable{ssh}, - }, nil) - require.NoError(t, err) - - // valid socket - st = llb.Image("alpine:latest"). - Run(llb.Shlex(`apk add --no-cache openssh`)). - Run(llb.Shlex(`sh -c 'echo -n $SSH_AUTH_SOCK > /out/sock && ssh-add -l > /out/out'`), - llb.AddSSHSocket()) - - 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, - }, - }, - Session: []session.Attachable{ssh}, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(filepath.Join(destDir, "sock")) - require.NoError(t, err) - require.Equal(t, "/run/buildkit/ssh_agent.0", string(dt)) - - dt, err = os.ReadFile(filepath.Join(destDir, "out")) - require.NoError(t, err) - require.Contains(t, string(dt), "2048") - require.Contains(t, string(dt), "(RSA)") - - // forbidden command - st = llb.Image("alpine:latest"). - Run(llb.Shlex(`apk add --no-cache openssh`)). - Run(llb.Shlex(`sh -c 'ssh-keygen -f /tmp/key -N "" && ssh-add -k /tmp/key 2> /out/out || true'`), - llb.AddSSHSocket()) - - out = st.AddMount("/out", llb.Scratch()) - def, err = out.Marshal(sb.Context()) - require.NoError(t, err) - - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destDir, - }, - }, - Session: []session.Attachable{ssh}, - }, nil) - require.NoError(t, err) - - dt, err = os.ReadFile(filepath.Join(destDir, "out")) - require.NoError(t, err) - require.Contains(t, string(dt), "agent refused operation") - - // valid socket from key on disk - st = llb.Image("alpine:latest"). - Run(llb.Shlex(`apk add --no-cache openssh`)). - Run(llb.Shlex(`sh -c 'ssh-add -l > /out/out'`), - llb.AddSSHSocket()) - - out = st.AddMount("/out", llb.Scratch()) - def, err = out.Marshal(sb.Context()) - require.NoError(t, err) - - k, err = rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - dt = pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(k), - }, - ) - - tmpDir := t.TempDir() - - err = os.WriteFile(filepath.Join(tmpDir, "key"), dt, 0600) - require.NoError(t, err) - - ssh, err = sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ - Paths: []string{filepath.Join(tmpDir, "key")}, - }}) - require.NoError(t, err) - - destDir = t.TempDir() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destDir, - }, - }, - Session: []session.Attachable{ssh}, - }, nil) - require.NoError(t, err) - - dt, err = os.ReadFile(filepath.Join(destDir, "out")) - require.NoError(t, err) - require.Contains(t, string(dt), "2048") - require.Contains(t, string(dt), "(RSA)") -} - -func testRawSocketMount(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - dir := t.TempDir() - sockPath := filepath.Join(dir, "test.sock") - listener := net.ListenConfig{} - l, err := listener.Listen(context.TODO(), "unix", sockPath) - require.NoError(t, err) - defer l.Close() - - var called atomic.Bool - srv := &http.Server{ - ReadHeaderTimeout: 10 * time.Second, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - called.Store(true) - w.WriteHeader(http.StatusOK) - }), - } - go srv.Serve(l) - defer srv.Close() - - st := llb.Image("alpine:latest"). - Run(llb.Shlex(`apk add --no-cache curl`)). - Run(llb.Shlex(`curl --unix-socket /tmp/test.sock http://./foo`), - llb.AddSSHSocket(llb.SSHSocketTarget("/tmp/test.sock")), - ).Root() - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - raw, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{ - Paths: []string{sockPath}, - Raw: true, - }}) - require.NoError(t, err) - - ch := make(chan *SolveStatus) - go func() { - for status := range ch { - for _, l := range status.Logs { - t.Log(string(l.Data)) - } - } - }() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Session: []session.Attachable{raw}, - }, ch) - require.NoError(t, err) - require.True(t, called.Load(), "server should have been called") -} - -/* -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 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`) -} - -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 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 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 testPushByDigest(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data"))) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - name := registry + "/foo/bar" - - resp, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": name, - "push": "true", - "push-by-digest": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - _, _, err = contentutil.ProviderFromRef(name + ":latest") - require.Error(t, err) - - desc, _, err := contentutil.ProviderFromRef(name + "@" + resp.ExporterResponse[exptypes.ExporterImageDigestKey]) - require.NoError(t, err) - - require.Equal(t, resp.ExporterResponse[exptypes.ExporterImageDigestKey], desc.Digest.String()) - require.Equal(t, ocispecs.MediaTypeImageManifest, desc.MediaType) - require.Greater(t, desc.Size, int64(0)) -} - -func testPushProgressSameVertex(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data"))) - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - imageName := registry + "/foo/bar:latest" - - var vertexes []*Vertex - var statuses []*VertexStatus - ch := make(chan *SolveStatus) - eg, ctx := errgroup.WithContext(sb.Context()) - eg.Go(func() error { - for { - select { - case <-ctx.Done(): - return context.Cause(ctx) - case ss, ok := <-ch: - if !ok { - return nil - } - vertexes = append(vertexes, ss.Vertexes...) - statuses = append(statuses, ss.Statuses...) - } - } - }) - eg.Go(func() error { - _, err := c.Solve(ctx, def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": imageName, - "push": "true", - }, - }, - }, - }, ch) - return err - }) - require.NoError(t, eg.Wait()) - - exportVertexes := map[digest.Digest]struct{}{} - pushVertexes := map[digest.Digest]struct{}{} - pushManifestPrefix := "pushing manifest for " + imageName + "@" - - for _, v := range vertexes { - if v.Name == "exporting to image" { - exportVertexes[v.Digest] = struct{}{} - } - } - - for _, st := range statuses { - switch { - case st.ID == "pushing layers": - pushVertexes[st.Vertex] = struct{}{} - case strings.HasPrefix(st.ID, pushManifestPrefix): - pushVertexes[st.Vertex] = struct{}{} - } - } - - require.Len(t, exportVertexes, 1, "expected exactly one export vertex in status stream") - require.NotEmpty(t, pushVertexes, "expected at least one push status in status stream") - - var exportVertex digest.Digest - for v := range exportVertexes { - exportVertex = v - } - for v := range pushVertexes { - require.Equal(t, exportVertex, v, "push statuses should use the same vertex as exporting to image") - } - - hasPushManifest := false - for _, st := range statuses { - if strings.HasPrefix(st.ID, pushManifestPrefix) { - hasPushManifest = true - require.Equal(t, exportVertex, st.Vertex, "pushing manifest should use the same vertex as exporting to image") - } - } - require.True(t, hasPushManifest, "expected pushing manifest status in status stream") -} - -func testPullWithDigestCheck(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.Scratch().File(llb.Mkfile("foo", 0600, []byte("data1"))) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - name1 := registry + "/foo/bar:v1.0.0" - - resp, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": name1, - "push": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst1Str := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - dgst1, err := digest.Parse(dgst1Str) - require.NoError(t, err) - - st = llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data2"))) - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - name2 := registry + "/foo/bar:v2.0.0" - - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": name2, - "push": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst2str := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - dgst2, err := digest.Parse(dgst2str) - require.NoError(t, err) - - require.NotEqual(t, dgst1, dgst2) - - // if digest is set in ref then pull happens only by the digest - st = llb.Image(name2 + "@" + dgst1.String()) - 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, "foo")) - require.NoError(t, err) - require.Equal(t, "data1", string(dt)) - - // if digest is set by checksum then pull happens by tag - st = llb.Image(name2, llb.WithImageChecksum(dgst2)) - 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, "foo")) - require.NoError(t, err) - require.Equal(t, "data2", string(dt)) - - // if checksum doesn't match then pull fails - st = llb.Image(name2, llb.WithImageChecksum(dgst1)) - 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(), fmt.Sprintf("image digest %s for %s does not match expected checksum %s", dgst2, name2, dgst1)) -} - -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 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 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 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) -} - -/* -testSecretMounts verifies file-based secret mounts: content readability, optional/required -behavior, custom permissions, and empty secrets. Skipped on Windows because BuildKit uses -tmpfs for secret mounts, which Windows does not support. See testSecretEnv for a -cross-platform alternative using environment-based secrets. -*/ -func testSecretMounts(t *testing.T, sb integration.Sandbox) { - // Windows vs Linux secret implementation differences: - // - // Linux: Secrets are mounted as files using tmpfs at /run/secrets/ (RAM-based, encrypted) - // - // Windows: Secrets are stored in C:\ProgramData\Docker\internal\secrets (clear text on disk) - // Symbolic links point to the desired target (default: C:\ProgramData\Docker\secrets) - // Windows does NOT support tmpfs or non-directory file bind-mounts - // UID/GID/mode options are NOT supported on Windows - // Recommend BitLocker for at-rest encryption - // - // BuildKit Issue: The "invalid windows mount type: 'tmpfs'" error occurs because BuildKit - // currently tries to use tmpfs for all secret mounts. This needs to be fixed in BuildKit's - // secret mount implementation to use the Windows symlink approach instead. - - // For now, this test is Linux-only until BuildKit properly implements Windows secret mounts - // without tmpfs. Use testSecretEnv for Windows-compatible environment-based secrets. - integration.SkipOnPlatform(t, "windows", "Windows does not support tmpfs for secret mounts") - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - // Test 1: Basic secret mount with content verification (Linux only) - st := llb.Image("busybox:latest"). - Run(llb.Shlex(`sh -c 'mount | grep mysecret | grep "type tmpfs" && [ "$(cat /run/secrets/mysecret)" = 'foo-secret' ]'`), llb.AddSecret("/run/secrets/mysecret")) - - 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{ - "/run/secrets/mysecret": []byte("foo-secret"), - })}, - }, nil) - require.NoError(t, err) - - // Test 2: Optional secret - mount should not exist when secret not present in SolveOpt - st = llb.Image("busybox:latest"). - Run(llb.Shlex(`test ! -f /run/secrets/mysecret2`), llb.AddSecret("/run/secrets/mysecret2", llb.SecretOptional)) - - 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{})}, - }, nil) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - // Test 3: Required secret missing - should error - st = llb.Image("busybox:latest"). - Run(llb.Shlex(`echo secret3`), llb.AddSecret("/run/secrets/mysecret3")) - - 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{})}, - }, nil) - require.Error(t, err) - - // Test 4: Secret with custom ID and file permissions - st = llb.Image("busybox:latest"). - Run(llb.Shlex(`sh -c '[ "$(stat -c "%u %g %f" /run/secrets/mysecret4)" = "1 1 81ff" ]' `), llb.AddSecret("/run/secrets/mysecret4", llb.SecretID("mysecret"), llb.SecretFileOpt(1, 1, 0777))) - - 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{ - "mysecret": []byte("pw"), - })}, - }, nil) - require.NoError(t, err) - - // Test 5: Empty secret still creates secret file - st = llb.Image("busybox:latest"). - Run(llb.Shlex(`test -f /run/secrets/mysecret5`), llb.AddSecret("/run/secrets/mysecret5", llb.SecretID("mysecret"))) - - 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{ - "mysecret": []byte(""), - })}, - }, nil) - require.NoError(t, err) -} - -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 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 -} - -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 (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) - }) -} - -/* -testSecretEnv verifies that secrets can be injected as environment variables during a build. -It tests four scenarios: (1) a provided secret is accessible via env var, (2) an optional -secret that is not provided results in an unset/empty env var, (3) a required secret that -is not provided causes an error, and (4) multiple secrets with custom IDs are resolved correctly. -Works on both Linux (busybox) and Windows (nanoserver). -*/ -func testSecretEnv(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") - var st llb.ExecState - - // Test 1: Verify secret value is accessible as environment variable - switch imgName { - case "nanoserver:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`cmd /C "if "%MY_SECRET%"=="foo-secret" (exit 0) else (exit 1)"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) - case "busybox:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`sh -c '[ "$(echo ${MY_SECRET})" = 'foo-secret' ]'`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) - } - - 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{ - "MY_SECRET": []byte("foo-secret"), - })}, - }, nil) - require.NoError(t, err) - - // Test 2: Optional secret not provided should be unset/empty - switch imgName { - case "nanoserver:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`cmd /C "if not defined MY_SECRET (exit 0) else (exit 1)"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true), llb.SecretOptional)) - case "busybox:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`sh -c '[ -z "${MY_SECRET}" ]'`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true), llb.SecretOptional)) - } - - 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{})}, - }, nil) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - // Test 3: Required secret not provided should error - switch imgName { - case "nanoserver:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`cmd /C "echo foo"`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) - case "busybox:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`echo foo`), llb.AddSecret("MY_SECRET", llb.SecretAsEnv(true))) - } - - 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{})}, - }, nil) - require.Error(t, err) - - // Test 4: Multiple secrets with custom IDs - switch imgName { - case "nanoserver:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`cmd /C "if "%MYPASSWORD%-%MYTOKEN%"=="pw-token" (exit 0) else (exit 1)"`), - llb.AddSecret("MYPASSWORD", llb.SecretID("pass"), llb.SecretAsEnv(true)), - llb.AddSecret("MYTOKEN", llb.SecretAsEnv(true)), - ) - case "busybox:latest": - st = llb.Image(imgName). - Run(llb.Shlex(`sh -c '[ "$(echo ${MYPASSWORD}-${MYTOKEN})" = "pw-token" ]' `), - llb.AddSecret("MYPASSWORD", llb.SecretID("pass"), llb.SecretAsEnv(true)), - llb.AddSecret("MYTOKEN", llb.SecretAsEnv(true)), - ) - } - - 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{ - "pass": []byte("pw"), - "MYTOKEN": []byte("token"), - })}, - }, nil) - require.NoError(t, err) -} - -func testTmpfsMounts(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 'mount | grep /foobar | grep "type tmpfs" && touch /foobar/test'`), llb.AddMount("/foobar", llb.Scratch(), llb.Tmpfs())) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -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 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 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 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) -} - -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) -} - -// 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) -} - -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 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) -} - -// 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 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 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) -} - -// 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) - } -} - -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 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) -} - -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 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) -} - -func testBuildMultiMount(t *testing.T, sb integration.Sandbox) { - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - imgAlpine := integration.UnixOrWindows( - "docker.io/library/alpine:latest", - "nanoserver", - ) - imgBusybox := integration.UnixOrWindows( - "docker.io/library/busybox:latest", - "nanoserver", - ) - cmdStr1 := integration.UnixOrWindows( - "/bin/ls -l", - "cmd /C dir", - ) - cmdStr2 := integration.UnixOrWindows( - "/bin/cp -a /busybox/etc/passwd baz", - `cmd /C copy C:\\busybox\\License.txt baz`, - ) - - alpine := llb.Image(imgAlpine) - ls := alpine.Run(llb.Shlex(cmdStr1)) - busybox := llb.Image(imgBusybox) - cp := ls.Run(llb.Shlex(cmdStr2)) - cp.AddMount("/busybox", busybox) - - def, err := cp.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) -} - -func testBuildExportScratch(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) - 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) - - makeFrontend := func(ps []string) func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - return func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - st := llb.Scratch() - def, err := st.Marshal(sb.Context()) - require.NoError(t, 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() - if ps == nil { - res.SetRef(ref) - } else { - for _, p := range ps { - res.AddRef(p, ref) - } - - expPlatforms := &exptypes.Platforms{ - Platforms: make([]exptypes.Platform, len(ps)), - } - for i, pk := range ps { - p := platforms.MustParse(pk) - - img := ocispecs.Image{ - Platform: p, - Config: ocispecs.ImageConfig{ - Labels: map[string]string{ - "foo": "i am platform " + platforms.Format(p), - }, - }, - } - config, err := json.Marshal(img) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal image config") - } - res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, pk), config) - - expPlatforms.Platforms[i] = exptypes.Platform{ - ID: pk, - Platform: p, - } - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - } - - return res, nil - } - } - - target := registry + "/buildkit/build/exporter:withnocompressed" - - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "unpack": "true", - "compression": "uncompressed", - "attest:provenance": "mode=max", - }, - }, - }, - }, "", makeFrontend(nil), nil) - require.NoError(t, err) - - targetMulti := registry + "/buildkit/build/exporter-multi:withnocompressed" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": targetMulti, - "push": "true", - "unpack": "true", - "compression": "uncompressed", - "attest:provenance": "mode=max", - }, - }, - }, - }, "", makeFrontend([]string{"linux/amd64", "linux/arm64"}), nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Len(t, imgs.Images, 1) - img := imgs.Images[0] - require.Empty(t, img.Layers) - require.True(t, platforms.Only(platforms.DefaultSpec()).Match(img.Img.Platform)) - - desc, provider, err = contentutil.ProviderFromRef(targetMulti) - require.NoError(t, err) - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Len(t, imgs.Images, 2) - img = imgs.Find("linux/amd64") - require.Empty(t, img.Layers) - require.Equal(t, "linux/amd64", platforms.Format(img.Img.Platform)) - require.Equal(t, "i am platform linux/amd64", img.Img.Config.Labels["foo"]) - img = imgs.Find("linux/arm64") - require.Empty(t, img.Layers) - require.Equal(t, "linux/arm64", platforms.Format(img.Img.Platform)) - require.Equal(t, "i am platform linux/arm64", img.Img.Config.Labels["foo"]) -} - -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 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") - } -} - -// 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 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")) -} - -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 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 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 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") -} - -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 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) - } -} - -func testOCIExporter(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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - target := "example.com/buildkit/testoci:latest" - for _, tc := range []struct { - name string - exporter string - attrs map[string]string - indexMediaType string - manifestMediaType string - configMediaType string - layerMediaType string - }{ - { - name: ExporterOCI, - exporter: ExporterOCI, - attrs: map[string]string{}, - indexMediaType: ocispecs.MediaTypeImageIndex, - manifestMediaType: ocispecs.MediaTypeImageManifest, - configMediaType: ocispecs.MediaTypeImageConfig, - layerMediaType: ocispecs.MediaTypeImageLayerGzip, - }, - { - name: ExporterDocker, - exporter: ExporterDocker, - attrs: map[string]string{ - "name": target, - }, - indexMediaType: ocispecs.MediaTypeImageIndex, - manifestMediaType: ocispecs.MediaTypeImageManifest, - configMediaType: ocispecs.MediaTypeImageConfig, - layerMediaType: ocispecs.MediaTypeImageLayerGzip, - }, - { - name: "docker-oci-mediatypes-false", - exporter: ExporterDocker, - attrs: map[string]string{ - "name": target, - "oci-mediatypes": "false", - }, - indexMediaType: ocispecs.MediaTypeImageIndex, - manifestMediaType: images.MediaTypeDockerSchema2Manifest, - configMediaType: images.MediaTypeDockerSchema2Config, - layerMediaType: images.MediaTypeDockerSchema2LayerGzip, - }, - } { - t.Run(tc.name, func(t *testing.T) { - 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: tc.exporter, - Attrs: tc.attrs, - 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) - - _, 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, tc.indexMediaType, index.MediaType) - require.Equal(t, 1, len(index.Manifests)) - require.Equal(t, tc.manifestMediaType, index.Manifests[0].MediaType) - - var mfst ocispecs.Manifest - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst) - require.NoError(t, err) - require.Equal(t, tc.manifestMediaType, mfst.MediaType) - require.Equal(t, tc.configMediaType, mfst.Config.MediaType) - require.Equal(t, 2, len(mfst.Layers)) - for _, layer := range mfst.Layers { - require.Equal(t, tc.layerMediaType, layer.MediaType) - } - - var ociimg ocispecs.Image - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Config.Digest.Hex()].Data, &ociimg) - require.NoError(t, err) - require.Equal(t, "layers", ociimg.RootFS.Type) - require.Equal(t, 2, len(ociimg.RootFS.DiffIDs)) - - _, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[0].Digest.Hex()] - require.True(t, ok) - _, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[1].Digest.Hex()] - require.True(t, ok) - - if tc.exporter != ExporterDocker { - return - } - - var dockerMfst []struct { - Config string - RepoTags []string - Layers []string - } - err = json.Unmarshal(m["manifest.json"].Data, &dockerMfst) - require.NoError(t, err) - require.Equal(t, 1, len(dockerMfst)) - - _, ok = m[dockerMfst[0].Config] - require.True(t, ok) - require.Equal(t, 2, len(dockerMfst[0].Layers)) - require.Equal(t, 1, len(dockerMfst[0].RepoTags)) - require.Equal(t, target, dockerMfst[0].RepoTags[0]) - - for _, l := range dockerMfst[0].Layers { - _, ok := m[l] - require.True(t, ok) - } - }) - } - - checkAllReleasable(t, c, sb, true) -} - -func testOCIExporterContentStore(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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - for _, exp := range []string{ExporterOCI, ExporterDocker} { - destDir := t.TempDir() - target := "example.com/buildkit/testoci:latest" - - outTar := filepath.Join(destDir, "out.tar") - outW, err := os.Create(outTar) - require.NoError(t, err) - attrs := map[string]string{} - if exp == ExporterDocker { - attrs["name"] = target - } - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: exp, - Attrs: attrs, - Output: fixedWriteCloser(outW), - }, - }, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(outTar) - require.NoError(t, err) - m, err := testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - checkStore := func(dir string) { - err = filepath.Walk(dir, func(filename string, fi os.FileInfo, err error) error { - filename = strings.TrimPrefix(filename, dir) - filename = strings.Trim(filename, "/") - if filename == "" || filename == "ingest" { - return nil - } - - if fi.IsDir() { - require.Contains(t, m, filename+"/") - } else { - require.Contains(t, m, filename) - if filename == ocispecs.ImageIndexFile { - // this file has a timestamp in it, so we can't compare - return nil - } - f, err := os.Open(path.Join(dir, filename)) - require.NoError(t, err) - data, err := io.ReadAll(f) - require.NoError(t, err) - require.Equal(t, m[filename].Data, data) - } - return nil - }) - require.NoError(t, err) - } - - outDir := filepath.Join(destDir, "out.d") - attrs = map[string]string{ - "tar": "false", - } - if exp == ExporterDocker { - attrs["name"] = target - } - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: exp, - Attrs: attrs, - OutputDir: outDir, - }, - }, - }, nil) - require.NoError(t, err) - checkStore(outDir) - - outStoreDir := filepath.Join(destDir, "store.d") - store, err := local.NewStore(outStoreDir) - require.NoError(t, err) - attrs = map[string]string{ - "tar": "false", - } - if exp == ExporterDocker { - attrs["name"] = target - } - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: exp, - Attrs: attrs, - OutputStore: store, - }, - }, - }, nil) - require.NoError(t, err) - checkStore(outDir) - } - - checkAllReleasable(t, c, sb, true) -} - -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 testSourceDateEpochLayerTimestamps(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) - 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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - 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) - - tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), - }, - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Output: fixedWriteCloser(outW), - }, - }, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(out) - require.NoError(t, err) - - tmsX, err := readImageTimestamps(dt) - require.NoError(t, err) - tms := tmsX.FromImage - - require.Equal(t, 3, len(tms)) - - expected := tm.UTC().Format(time.RFC3339Nano) - require.Equal(t, expected, tms[0]) - require.Equal(t, expected, tms[1]) - require.Equal(t, expected, tms[2]) - - checkAllReleasable(t, c, sb, true) -} - -func testSourceDateEpochClamp(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) - requiresLinux(t) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - var bboxConfig []byte - _, err = c.Build(sb.Context(), SolveOpt{}, "", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - _, _, bboxConfig, err = c.ResolveImageConfig(ctx, "docker.io/library/busybox:latest", sourceresolver.Opt{}) - if err != nil { - return nil, err - } - return nil, nil - }, nil) - require.NoError(t, err) - - m := map[string]json.RawMessage{} - require.NoError(t, json.Unmarshal(bboxConfig, &m)) - delete(m, "created") - bboxConfig, err = json.Marshal(m) - require.NoError(t, err) - - busybox, err := llb.Image("busybox:latest").WithImageConfig(bboxConfig) - require.NoError(t, err) - - def, err := busybox.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, - Attrs: map[string]string{ - exptypes.ExporterImageConfigKey: string(bboxConfig), - }, - Output: fixedWriteCloser(outW), - }, - }, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(out) - require.NoError(t, err) - - busyboxTmsX, err := readImageTimestamps(dt) - require.NoError(t, err) - busyboxTms := busyboxTmsX.FromImage - - require.Greater(t, len(busyboxTms), 1) - bboxLayerLen := len(busyboxTms) - 1 - - tm, err := time.Parse(time.RFC3339Nano, busyboxTms[1]) - require.NoError(t, err) - - next := tm.Add(time.Hour).Truncate(time.Second) - - st := busybox.Run(llb.Shlex("touch /foo")) - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - out = filepath.Join(destDir, "out.tar") - outW, err = os.Create(out) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", next.Unix()), - }, - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Attrs: map[string]string{ - exptypes.ExporterImageConfigKey: string(bboxConfig), - }, - Output: fixedWriteCloser(outW), - }, - }, - }, nil) - require.NoError(t, err) - - dt, err = os.ReadFile(out) - require.NoError(t, err) - - tmsX, err := readImageTimestamps(dt) - require.NoError(t, err) - tms := tmsX.FromImage - - require.Equal(t, len(tms), bboxLayerLen+2) - - expected := next.UTC().Format(time.RFC3339Nano) - require.Equal(t, expected, tms[0]) - require.Equal(t, busyboxTms[1], tms[1]) - require.Equal(t, expected, tms[bboxLayerLen+1]) - require.Equal(t, expected, tmsX.FromAnnotation) - - checkAllReleasable(t, c, sb, true) -} - -// testSourceDateEpochReset tests that the SOURCE_DATE_EPOCH is reset if exporter option is set -func testSourceDateEpochReset(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureSourceDateEpoch) - 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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - 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) - - tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), - }, - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Attrs: map[string]string{"source-date-epoch": ""}, - Output: fixedWriteCloser(outW), - }, - }, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(out) - require.NoError(t, err) - - tmsX, err := readImageTimestamps(dt) - require.NoError(t, err) - tms := tmsX.FromImage - - require.Equal(t, 3, len(tms)) - - expected := tm.UTC().Format(time.RFC3339Nano) - require.NotEqual(t, expected, tms[0]) - require.NotEqual(t, expected, tms[1]) - require.NotEqual(t, expected, tms[2]) - - require.Equal(t, tms[0], tms[2]) - require.NotEqual(t, tms[2], tms[1]) - - checkAllReleasable(t, c, sb, true) -} - -func testSourceDateEpochLocalExporter(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) - 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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - destDir, err := os.MkdirTemp("", "buildkit") - require.NoError(t, err) - defer os.RemoveAll(destDir) - - tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), - }, - 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, fi.ModTime().Format(time.RFC3339), tm.UTC().Format(time.RFC3339)) - - fi, err = os.Stat(filepath.Join(destDir, "bar")) - require.NoError(t, err) - require.Equal(t, fi.ModTime().Format(time.RFC3339), tm.UTC().Format(time.RFC3339)) - - checkAllReleasable(t, c, sb, true) -} - -func testSourceDateEpochTarExporter(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) - 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 "echo -n first > foo"`) - run(`sh -c "echo -n second > bar"`) - - 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) - - tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), - }, - 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) - - require.Equal(t, 2, len(m)) - - require.Equal(t, tm.Format(time.RFC3339), m["foo"].Header.ModTime.Format(time.RFC3339)) - require.Equal(t, tm.Format(time.RFC3339), m["bar"].Header.ModTime.Format(time.RFC3339)) - - checkAllReleasable(t, c, sb, true) -} - -func testSourceDateEpochImageExporter(t *testing.T, sb integration.Sandbox) { - cdAddress := sb.ContainerdAddress() - if cdAddress == "" { - t.SkipNow() - } - - // https://github.com/containerd/containerd/commit/133ddce7cf18a1db175150e7a69470dea1bb3132 - minVer := "v1.7.0-beta.1" - cdVersion := containerdutil.GetVersion(t, cdAddress) - // Normalize containerd version to semver format (add v prefix if missing) - normalizedCdVersion := cdVersion - if !strings.HasPrefix(cdVersion, "v") { - normalizedCdVersion = "v" + cdVersion - } - if semver.Compare(normalizedCdVersion, minVer) < 0 { - t.Skipf("containerd version %q does not satisfy minimal version %q", cdVersion, minVer) - } - - workers.CheckFeatureCompat(t, sb, workers.FeatureSourceDateEpoch) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - - type testVars struct { - baseImgName string - st llb.State - cmdPrefix string - } - - baseImg := llb.Image(integration.UnixOrWindows("busybox:latest", "nanoserver:latest")) - v := integration.UnixOrWindows( - testVars{ - baseImgName: "busybox:latest", - st: llb.Scratch(), - cmdPrefix: `sh -c "echo -n`, - }, - testVars{ - baseImgName: "nanoserver:latest", - st: baseImg, - cmdPrefix: `cmd /C "echo`, - }, - ) - - st := v.st - run := func(cmd string) { - st = baseImg.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - } - - run(fmt.Sprintf(`%s first> foo"`, v.cmdPrefix)) - run(fmt.Sprintf(`%s second> bar"`, v.cmdPrefix)) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - name := strings.ToLower(path.Base(t.Name())) - tm := time.Date(2015, time.October, 21, 7, 28, 0, 0, time.UTC) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: map[string]string{ - "build-arg:SOURCE_DATE_EPOCH": fmt.Sprintf("%d", tm.Unix()), - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": name, - }, - }, - }, - }, nil) - require.NoError(t, err) - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - client, err := newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - - img, err := client.GetImage(ctx, name) - require.NoError(t, err) - require.Equal(t, tm, img.Metadata().CreatedAt) - - err = client.ImageService().Delete(ctx, name, images.SynchronousDelete()) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) -} - -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 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 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) -} - -// 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) -} - -// testBuildExportWithForeignLayer verifies foreign (non-distributable) layer handling during -// image export. propagate=1 preserves foreign media type and URLs; propagate=0 converts to -// regular layers. Skipped on Windows: the test image is Linux-only and BuildKit's Windows -// exporter does not preserve foreign layer media types. -func testBuildExportWithForeignLayer(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows", "test image is Linux-only and BuildKit's Windows exporter strips foreign layer media types") - workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - st := llb.Image("cpuguy83/buildkit-foreign:latest") - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - t.Run("propagate=1", func(t *testing.T) { - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - target := registry + "/buildkit/build/exporter/foreign:latest" - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "oci-mediatypes": "false", - "prefer-nondist-layers": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - - resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true}) - name, desc, err := resolver.Resolve(ctx, target) - require.NoError(t, err) - - fetcher, err := resolver.Fetcher(ctx, name) - require.NoError(t, err) - mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any()) - require.NoError(t, err) - - require.Equal(t, 2, len(mfst.Layers)) - require.Equal(t, images.MediaTypeDockerSchema2LayerForeign, mfst.Layers[0].MediaType) - require.Len(t, mfst.Layers[0].URLs, 1) - require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType) - - rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size}) - require.NoError(t, err) - defer rc.Close() - - // `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request. - // The request is only made when we attempt to read from the reader. - buf := make([]byte, 1) - _, err = rc.Read(buf) - require.Truef(t, cerrdefs.IsNotFound(err), "expected error for blob that should not be in registry: %s, %v", mfst.Layers[0].Digest, err) - }) - t.Run("propagate=0", func(t *testing.T) { - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/build/exporter/noforeign:latest" - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "oci-mediatypes": "false", - }, - }, - }, - }, nil) - require.NoError(t, err) - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - - resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true}) - name, desc, err := resolver.Resolve(ctx, target) - require.NoError(t, err) - - fetcher, err := resolver.Fetcher(ctx, name) - require.NoError(t, err) - - mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any()) - require.NoError(t, err) - - require.Equal(t, 2, len(mfst.Layers)) - require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[0].MediaType) - require.Len(t, mfst.Layers[0].URLs, 0) - require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType) - - rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size}) - require.NoError(t, err) - defer rc.Close() - - // `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request. - // The request is only made when we attempt to read from the reader. - buf := make([]byte, 1) - _, err = rc.Read(buf) - require.NoError(t, err) - }) -} - -func testBuildExportWithUncompressed(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) - requiresLinux(t) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - busybox := llb.Image("busybox:latest") - cmd := `sh -e -c "echo -n uncompressed > data"` - - st := llb.Scratch() - st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - - 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/build/exporter:withnocompressed" - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "compression": "uncompressed", - }, - }, - }, - }, nil) - require.NoError(t, err) - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - cdAddress := sb.ContainerdAddress() - var client *ctd.Client - if cdAddress != "" { - client, err = newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - - img, err := client.GetImage(ctx, target) - require.NoError(t, err) - mfst, err := images.Manifest(ctx, client.ContentStore(), img.Target(), nil) - require.NoError(t, err) - require.Equal(t, 1, len(mfst.Layers)) - require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType) - } - - // new layer with gzip compression - targetImg := llb.Image(target) - cmd = `sh -e -c "echo -n gzip > data"` - st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", targetImg) - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - compressedTarget := registry + "/buildkit/build/exporter:withcompressed" - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": compressedTarget, - "push": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - allCompressedTarget := registry + "/buildkit/build/exporter:withallcompressed" - _, err = c.Solve(context.TODO(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": allCompressedTarget, - "push": "true", - "compression": "gzip", - "force-compression": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - if cdAddress == "" { - t.Skip("rest of test requires containerd worker") - } - - err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) - require.NoError(t, err) - err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete()) - require.NoError(t, err) - err = client.ImageService().Delete(ctx, allCompressedTarget, images.SynchronousDelete()) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) - - // check if the new layer is compressed with compression option - img, err := client.Pull(ctx, compressedTarget) - require.NoError(t, err) - - dt, err := content.ReadBlob(ctx, img.ContentStore(), img.Target()) - require.NoError(t, err) - - mfst := struct { - MediaType string `json:"mediaType,omitempty"` - ocispecs.Manifest - }{} - - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - require.Equal(t, 2, len(mfst.Layers)) - require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType) - require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) - require.NoError(t, err) - - m, err := testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - item, ok := m["data"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("uncompressed"), item.Data) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest}) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - item, ok = m["data"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("gzip"), item.Data) - - err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete()) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) - - // check if all layers are compressed with force-compressoin option - img, err = client.Pull(ctx, allCompressedTarget) - require.NoError(t, err) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target()) - require.NoError(t, err) - - mfst = struct { - MediaType string `json:"mediaType,omitempty"` - ocispecs.Manifest - }{} - - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - require.Equal(t, 2, len(mfst.Layers)) - require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType) - require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - item, ok = m["data"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("uncompressed"), item.Data) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest}) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - item, ok = m["data"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("gzip"), item.Data) -} - -// testBuildExportZstd verifies OCI export with Zstd-compressed layers. -// It builds a simple layer, exports it with Zstd compression, and validates both -// OCI and Docker schema 2 media types. -func testBuildExportZstd(t *testing.T, sb integration.Sandbox) { - // Skipped on Windows: - // 1. AddMount() fails because Windows snapshots allow only a single mount per layer. - // 2. OCI export fails because windowsLcowDiff lacks the Compare method needed to - // generate compressed layer diffs. - integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', OCI export fails with 'windowsLcowDiff does not implement Compare method'") - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - busybox := llb.Image("busybox:latest") - cmd := `sh -e -c "echo -n zstd > data"` - - st := llb.Scratch() - st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - - 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), - Attrs: map[string]string{ - "compression": "zstd", - }, - }, - }, - // compression option should work even with inline cache exports - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - }, 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] - require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType) - - zstdLayerDigest := lastLayer.Digest.Hex() - require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4]) - - // repeat without oci mediatype - outW, err = os.Create(out) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Output: fixedWriteCloser(outW), - Attrs: map[string]string{ - "compression": "zstd", - "oci-mediatypes": "false", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dt, err = os.ReadFile(out) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index) - require.NoError(t, err) - - 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] - require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, lastLayer.MediaType) - - require.Equal(t, lastLayer.Digest.Hex(), zstdLayerDigest) -} - -// testPullZstdImage verifies pulling and re-exporting a Zstd-compressed image. -// It pushes a Zstd-compressed image to a registry, pulls it back, and validates -// the layer media types and Zstd magic bytes for both OCI and Docker schema 2. -func testPullZstdImage(t *testing.T, sb integration.Sandbox) { - // Skipped on Windows: - // 1. AddMount() fails because Windows snapshots allow only a single mount per layer. - // 2. Zstd-compressed push/export fails because windowsLcowDiff lacks the Compare - // method needed to generate compressed layer diffs. - integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', Zstd export fails with 'windowsLcowDiff does not implement Compare method'") - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush) - for _, ociMediaTypes := range []bool{true, false} { - t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t", ociMediaTypes), func(t *testing.T) { - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - busybox := llb.Image("busybox:latest") - cmd := `sh -e -c "echo -n zstd > data"` - - st := llb.Scratch() - st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - - 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/build/exporter:zstd" - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "compression": "zstd", - "oci-mediatypes": strconv.FormatBool(ociMediaTypes), - }, - }, - }, - }, nil) - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - - st = llb.Image(target).File(llb.Copy(llb.Image(target), "/data", "/zdata")) - 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), - Attrs: map[string]string{ - "oci-mediatypes": strconv.FormatBool(ociMediaTypes), - }, - }, - }, - }, 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) - - firstLayer := mfst.Layers[0] - if ociMediaTypes { - require.Equal(t, ocispecs.MediaTypeImageLayerZstd, firstLayer.MediaType) - } else { - require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, firstLayer.MediaType) - } - - zstdLayerDigest := firstLayer.Digest.Hex() - require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4]) - }) - } -} - -func testBuildPushAndValidate(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() - - 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 foo/sub; echo -n first > foo/sub/bar; chmod 0741 foo;"`) - run(`true`) // this doesn't create a layer - run(`sh -c "echo -n second > foo/sub/baz"`) - - 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/testpush: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) - - // test existence of the image with next build - firstBuild := llb.Image(target) - - def, err = firstBuild.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, "foo/sub/bar")) - require.NoError(t, err) - require.Equal(t, dt, []byte("first")) - - dt, err = os.ReadFile(filepath.Join(destDir, "foo/sub/baz")) - require.NoError(t, err) - require.Equal(t, dt, []byte("second")) - - fi, err := os.Stat(filepath.Join(destDir, "foo")) - require.NoError(t, err) - require.Equal(t, 0741, int(fi.Mode()&0777)) - - checkAllReleasable(t, c, sb, false) - - // examine contents of exported tars (requires containerd) - cdAddress := sb.ContainerdAddress() - if cdAddress == "" { - t.Skip("rest of test requires containerd worker") - } - - // 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() - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - - // check image in containerd - _, err = client.ImageService().Get(ctx, target) - require.NoError(t, err) - - // deleting image should release all content - err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) - - img, err := client.Pull(ctx, target) - require.NoError(t, err) - - desc, err := img.Config(ctx) - require.NoError(t, err) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), desc) - require.NoError(t, err) - - var ociimg ocispecs.Image - err = json.Unmarshal(dt, &ociimg) - require.NoError(t, err) - - require.NotEqual(t, "", ociimg.OS) - require.NotEqual(t, "", ociimg.Architecture) - require.NotEqual(t, "", ociimg.Config.WorkingDir) - require.Equal(t, "layers", ociimg.RootFS.Type) - require.Equal(t, 3, len(ociimg.RootFS.DiffIDs)) - require.NotNil(t, ociimg.Created) - require.Less(t, time.Since(*ociimg.Created), 2*time.Minute) - require.Condition(t, func() bool { - for _, env := range ociimg.Config.Env { - if strings.HasPrefix(env, "PATH=") { - return true - } - } - return false - }) - - require.Equal(t, 3, len(ociimg.History)) - require.Contains(t, ociimg.History[0].CreatedBy, "foo/sub/bar") - require.Contains(t, ociimg.History[1].CreatedBy, "true") - require.Contains(t, ociimg.History[2].CreatedBy, "foo/sub/baz") - require.False(t, ociimg.History[0].EmptyLayer) - require.False(t, ociimg.History[1].EmptyLayer) - require.False(t, ociimg.History[2].EmptyLayer) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target()) - require.NoError(t, err) - - mfst := struct { - MediaType string `json:"mediaType,omitempty"` - ocispecs.Manifest - }{} - - err = json.Unmarshal(dt, &mfst) - require.NoError(t, err) - - require.Equal(t, ocispecs.MediaTypeImageManifest, mfst.MediaType) - require.Equal(t, 3, len(mfst.Layers)) - require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType) - require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest}) - require.NoError(t, err) - - m, err := testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - item, ok := m["foo/"] - require.True(t, ok) - require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) - require.Equal(t, 0741, int(item.Header.Mode&0777)) - - item, ok = m["foo/sub/"] - require.True(t, ok) - require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) - - item, ok = m["foo/sub/bar"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("first"), item.Data) - - _, ok = m["foo/sub/baz"] - require.False(t, ok) - - dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[2].Digest}) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, true) - require.NoError(t, err) - - item, ok = m["foo/sub/baz"] - require.True(t, ok) - require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag)) - require.Equal(t, []byte("second"), item.Data) - - item, ok = m["foo/"] - require.True(t, ok) - require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) - require.Equal(t, 0741, int(item.Header.Mode&0777)) - - item, ok = m["foo/sub/"] - require.True(t, ok) - require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag)) - - _, ok = m["foo/sub/bar"] - require.False(t, ok) -} - -func testStargzLazyRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheBackendRegistry, - workers.FeatureOCIExporter, - ) - requiresLinux(t) - cdAddress := sb.ContainerdAddress() - if cdAddress == "" || sb.Snapshotter() != "stargz" { - t.Skip("test requires containerd worker with stargz snapshotter") - } - - client, err := newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - var ( - imageService = client.ImageService() - contentStore = client.ContentStore() - ctx = namespaces.WithNamespace(sb.Context(), "buildkit") - ) - - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - destDir := t.TempDir() - - // Prepare stargz registry cache - orgImage := "docker.io/library/alpine:latest" - sgzCache := registry + "/stargz/alpinecache:" + identity.NewID() - baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"})) - def, err := baseDef.Marshal(sb.Context()) - require.NoError(t, err) - 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), - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzCache, - "compression": "estargz", - "oci-mediatypes": "true", - "force-compression": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - // clear all local state out - ensurePruneAll(t, c, sb) - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureDirectPush) - - // stargz layers should be lazy even for executing something on them - def, err = baseDef. - Run(llb.Args([]string{"sh", "-c", "cat /dev/urandom | head -c 100 | sha256sum > unique"})). - Marshal(sb.Context()) - require.NoError(t, err) - target := registry + "/buildkit/testlazyimage:" + identity.NewID() - resp, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "store": "true", - "oci-mediatypes": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzCache, - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzCache, - "compression": "estargz", - "oci-mediatypes": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") - 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) - - // Check if image layers are lazy. - // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. - var sgzLayers []ocispecs.Descriptor - for i, layer := range manifest.Layers[:len(manifest.Layers)-1] { - _, err = contentStore.Info(ctx, layer.Digest) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i) - sgzLayers = append(sgzLayers, layer) - } - require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") - - // The topmost(last) layer created by `Run` shouldn't be lazy - _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) - require.NoError(t, err) - - // Run build again and check if cache is reused - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "store": "true", - "oci-mediatypes": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzCache, - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - unique2, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2, "/unique") - require.NoError(t, err) - - require.Equal(t, dgst, dgst2) - require.Equal(t, unique, unique2) - - // clear all local state out - err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // stargz layers should be exportable - out = filepath.Join(destDir, "out2.tar") - outW, err = os.Create(out) - require.NoError(t, err) - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Output: fixedWriteCloser(outW), - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzCache, - }, - }, - }, - }, nil) - require.NoError(t, err) - - // Check if image layers are un-lazied - for _, layer := range sgzLayers { - _, err = contentStore.Info(ctx, layer.Digest) - require.NoError(t, err) - } - - ensurePruneAll(t, c, sb) -} - -func testStargzLazyInlineCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendInline, - workers.FeatureCacheBackendRegistry, - ) - requiresLinux(t) - cdAddress := sb.ContainerdAddress() - if cdAddress == "" || sb.Snapshotter() != "stargz" { - t.Skip("test requires containerd worker with stargz snapshotter") - } - - client, err := newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - var ( - imageService = client.ImageService() - contentStore = client.ContentStore() - ctx = namespaces.WithNamespace(sb.Context(), "buildkit") - ) - - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - // Prepare stargz inline cache - orgImage := "docker.io/library/alpine:latest" - sgzImage := registry + "/stargz/alpine:" + identity.NewID() - baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"})) - def, err := baseDef.Marshal(sb.Context()) - require.NoError(t, err) - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": sgzImage, - "push": "true", - "compression": "estargz", - "oci-mediatypes": "true", - "force-compression": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - }, nil) - require.NoError(t, err) - - // clear all local state out - err = imageService.Delete(ctx, sgzImage, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // stargz layers should be lazy even for executing something on them - def, err = baseDef. - Run(llb.Args([]string{"/bin/touch", "/bar"})). - Marshal(sb.Context()) - require.NoError(t, err) - target := registry + "/buildkit/testlazyimage:" + identity.NewID() - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "oci-mediatypes": "true", - "compression": "estargz", - "store": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzImage, - }, - }, - }, - }, 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) - - // Check if image layers are lazy. - // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. - var sgzLayers []ocispecs.Descriptor - for i, layer := range manifest.Layers[:len(manifest.Layers)-1] { - _, err = contentStore.Info(ctx, layer.Digest) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i) - sgzLayers = append(sgzLayers, layer) - } - require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") - - // The topmost(last) layer created by `Run` shouldn't be lazy - _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) - require.NoError(t, err) - - // clear all local state out - err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // stargz layers should be exportable - 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), - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": sgzImage, - }, - }, - }, - }, nil) - require.NoError(t, err) - - // Check if image layers are un-lazied - for _, layer := range sgzLayers { - _, err = contentStore.Info(ctx, layer.Digest) - require.NoError(t, err) - } - - ensurePruneAll(t, c, sb) -} - -func testStargzLazyPull(t *testing.T, sb integration.Sandbox) { - requiresLinux(t) - - cdAddress := sb.ContainerdAddress() - if cdAddress == "" || sb.Snapshotter() != "stargz" { - t.Skip("test requires containerd worker with stargz snapshotter") - } - - client, err := newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - var ( - imageService = client.ImageService() - contentStore = client.ContentStore() - ctx = namespaces.WithNamespace(sb.Context(), "buildkit") - ) - - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - // Prepare stargz image - orgImage := "docker.io/library/alpine:latest" - sgzImage := registry + "/stargz/alpine:" + identity.NewID() - def, err := llb.Image(orgImage).Marshal(sb.Context()) - require.NoError(t, err) - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": sgzImage, - "push": "true", - "compression": "estargz", - "oci-mediatypes": "true", - "force-compression": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - // clear all local state out - err = imageService.Delete(ctx, sgzImage, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // stargz layers should be lazy even for executing something on them - def, err = llb.Image(sgzImage). - Run(llb.Args([]string{"sh", "-c", "cat /dev/urandom | head -c 100 | sha256sum > unique"})). - Marshal(sb.Context()) - require.NoError(t, err) - target := registry + "/buildkit/testlazyimage:" + identity.NewID() - resp, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "oci-mediatypes": "true", - "store": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") - 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) - - // Check if image layers are lazy. - // The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer. - var sgzLayers []ocispecs.Descriptor - for _, layer := range manifest.Layers[:len(manifest.Layers)-1] { - _, err = contentStore.Info(ctx, layer.Digest) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) - sgzLayers = append(sgzLayers, layer) - } - require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull") - - // The topmost(last) layer created by `Run` shouldn't be lazy - _, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest) - require.NoError(t, err) - - // Run build again and check if cache is reused - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "store": "true", - "oci-mediatypes": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - unique2, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2, "/unique") - require.NoError(t, err) - - require.Equal(t, dgst, dgst2) - require.Equal(t, unique, unique2) - - // clear all local state out - err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // stargz layers should be exportable - 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) - - // Check if image layers are un-lazied - for _, layer := range sgzLayers { - _, err = contentStore.Info(ctx, layer.Digest) - require.NoError(t, err) - } - - ensurePruneAll(t, c, sb) -} - -func testLazyImagePush(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush) - 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 := integration.UnixOrWindows("busybox:latest", "nanoserver:latest") - def, err := llb.Image(sourceImage).Marshal(sb.Context()) - require.NoError(t, err) - - targetNoTag := registry + "/buildkit/testlazyimage:" - target := targetNoTag + "latest" - _, 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", - }, - }, - }, - }, nil) - require.NoError(t, err) - - imageService := client.ImageService() - contentStore := client.ContentStore() - - img, err := imageService.Get(ctx, target) - require.NoError(t, err) - - manifest, err := images.Manifest(ctx, contentStore, img.Target, nil) - require.NoError(t, err) - - for _, layer := range manifest.Layers { - _, err = contentStore.Info(ctx, layer.Digest) - require.NoError(t, err) - } - - // clear all local state out - err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // retag the image we just pushed with no actual changes, which - // should not result in the image getting un-lazied - def, err = llb.Image(target).Marshal(sb.Context()) - require.NoError(t, err) - - target2 := targetNoTag + "newtag" - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target2, - "push": "true", - "store": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - img, err = imageService.Get(ctx, target2) - require.NoError(t, err) - - manifest, err = images.Manifest(ctx, contentStore, img.Target, nil) - require.NoError(t, err) - - for _, layer := range manifest.Layers { - _, err = contentStore.Info(ctx, layer.Digest) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) - } - - // clear all local state out again - err = imageService.Delete(ctx, img.Name, images.SynchronousDelete()) - require.NoError(t, err) - checkAllReleasable(t, c, sb, true) - - // try a cross-repo push to same registry, which should still result in the - // image remaining lazy - target3 := registry + "/buildkit/testlazycrossrepo:latest" - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target3, - "push": "true", - "store": "true", - "unsafe-internal-store-allow-incomplete": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - img, err = imageService.Get(ctx, target3) - require.NoError(t, err) - - manifest, err = images.Manifest(ctx, contentStore, img.Target, nil) - require.NoError(t, err) - - for _, layer := range manifest.Layers { - _, err = contentStore.Info(ctx, layer.Digest) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) - } - - // check that a subsequent build can use the previously lazy image in an exec - cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0") - def, err = llb.Image(target2).Run(llb.Args([]string{cmdStr})).Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -// TODO: Re-enable the skipped test on Windows (see issue #6296) -func testZstdLocalCacheExport(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.FeatureCacheExport, workers.FeatureCacheBackendLocal) - 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 -e -c "echo -n zstd > data"`, - `cmd /C "echo zstd > C:\data"`, - ) - - st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) - st = llb.Image(imgName).Run(llb.Shlex(cmdStr), llb.Dir("/wd")).AddMount("/wd", st) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - destDir := t.TempDir() - destOutDir := t.TempDir() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destOutDir, - }, - }, - // compression option should work even with inline cache exports - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": destDir, - "image-manifest": "false", - "compression": "zstd", - }, - }, - }, - }, nil) - require.NoError(t, err) - - var index ocispecs.Index - dt, err := os.ReadFile(filepath.Join(destDir, ocispecs.ImageIndexFile)) - require.NoError(t, err) - err = json.Unmarshal(dt, &index) - require.NoError(t, err) - - var layerIndex ocispecs.Index - dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex())) - require.NoError(t, err) - err = json.Unmarshal(dt, &layerIndex) - require.NoError(t, err) - - lastLayer := layerIndex.Manifests[len(layerIndex.Manifests)-2] - require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType) - - zstdLayerDigest := lastLayer.Digest.Hex() - dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest)) - require.NoError(t, err) - require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, dt[:4]) -} - -func testCacheExportIgnoreError(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport) - 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) - cmd := integration.UnixOrWindows( - `sh -e -c "echo -n ignore-error > data"`, - "cmd /C echo ignore-error> data", - ) - - st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) - st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - tests := map[string]struct { - Exports []ExportEntry - CacheExports []CacheOptionsEntry - expectedErrors []string - }{ - "local-ignore-error": { - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: t.TempDir(), - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": "éèç", - }, - }, - }, - expectedErrors: []string{"failed to solve", "contains value with non-printable ASCII characters"}, - }, - "registry-ignore-error": { - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": "test-registry-ignore-error", - "push": "false", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": "fake-url:5000/myrepo:buildcache", - }, - }, - }, - expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"}, - }, - "s3-ignore-error": { - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: t.TempDir(), - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "s3", - Attrs: map[string]string{ - "endpoint_url": "http://fake-url:9000", - "bucket": "my-bucket", - "region": "us-east-1", - "access_key_id": "minioadmin", - "secret_access_key": "minioadmin", - "use_path_style": "true", - }, - }, - }, - expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"}, - }, - } - ignoreErrorValues := []bool{true, false} - for _, ignoreError := range ignoreErrorValues { - ignoreErrStr := strconv.FormatBool(ignoreError) - for n, test := range tests { - require.Equal(t, 1, len(test.Exports)) - require.Equal(t, 1, len(test.CacheExports)) - require.NotEmpty(t, test.CacheExports[0].Attrs) - test.CacheExports[0].Attrs["ignore-error"] = ignoreErrStr - testName := fmt.Sprintf("%s-%s", n, ignoreErrStr) - t.Run(testName, func(t *testing.T) { - switch n { - case "local-ignore-error": - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendLocal) - case "registry-ignore-error": - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendRegistry) - case "s3-ignore-error": - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendS3) - } - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: test.Exports, - CacheExports: test.CacheExports, - }, nil) - if ignoreError { - require.NoError(t, err) - } else { - require.Error(t, err) - for _, errStr := range test.expectedErrors { - require.Contains(t, err.Error(), errStr) - } - } - }) - } - } -} - -func testUncompressedLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendLocal, - ) - dir := t.TempDir() - im := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "src": dir, - }, - } - ex := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "dest": dir, - "compression": "uncompressed", - "force-compression": "true", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testUncompressedRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - im := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - } - ex := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - "compression": "uncompressed", - "force-compression": "true", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testZstdLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendLocal, - ) - dir := t.TempDir() - im := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "src": dir, - }, - } - ex := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "dest": dir, - "compression": "zstd", - "force-compression": "true", - "oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype. - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testImageManifestRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - im := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - } - ex := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - "oci-mediatypes": "true", - "mode": "max", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -// TODO: Re-enable the skipped test on Windows (see issue #6296) -func testZstdRegistryCacheImportExport(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.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - im := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - } - ex := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - "compression": "zstd", - "force-compression": "true", - "image-manifest": "false", - "oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype. - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testBasicCacheImportExport(t *testing.T, sb integration.Sandbox, cacheOptionsEntryImport, cacheOptionsEntryExport []CacheOptionsEntry) { - 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) { - baseState := integration.UnixOrWindows(busybox, st) - st = baseState.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) - } - - run(integration.UnixOrWindows( - `sh -c "echo -n foobar > const"`, - `cmd /C echo foobar > const`, - )) - run(integration.UnixOrWindows( - `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, - `cmd /C echo %RANDOM% > 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, - }, - }, - CacheExports: cacheOptionsEntryExport, - }, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(filepath.Join(destDir, "const")) - require.NoError(t, err) - newLine := integration.UnixOrWindows("", " \r\n") - require.Equal(t, "foobar"+newLine, string(dt)) - - dt, err = os.ReadFile(filepath.Join(destDir, "unique")) - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - - destDir = t.TempDir() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destDir, - }, - }, - CacheImports: cacheOptionsEntryImport, - }, nil) - require.NoError(t, err) - - dt2, err := os.ReadFile(filepath.Join(destDir, "const")) - require.NoError(t, err) - require.Equal(t, "foobar"+newLine, string(dt2)) - - dt2, err = os.ReadFile(filepath.Join(destDir, "unique")) - require.NoError(t, err) - require.Equal(t, string(dt), string(dt2)) -} - -func testBasicRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - o := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o}, []CacheOptionsEntry{o}) -} - -func testMultipleRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - o := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - } - o2 := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": target + "notexist", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o, o2}, []CacheOptionsEntry{o}) -} - -func testBasicLocalCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendLocal, - ) - dir := t.TempDir() - im := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "src": dir, - }, - } - ex := CacheOptionsEntry{ - Type: "local", - Attrs: map[string]string{ - "dest": dir, - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testBasicS3CacheImportExport(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendS3, - ) - - opts := helpers.MinioOpts{ - Region: "us-east-1", - AccessKeyID: "minioadmin", - SecretAccessKey: "minioadmin", - } - - s3Addr, s3Bucket, cleanup, err := helpers.NewMinioServer(t, sb, opts) - require.NoError(t, err) - defer cleanup() - - im := CacheOptionsEntry{ - Type: "s3", - Attrs: map[string]string{ - "region": opts.Region, - "access_key_id": opts.AccessKeyID, - "secret_access_key": opts.SecretAccessKey, - "bucket": s3Bucket, - "endpoint_url": s3Addr, - "use_path_style": "true", - }, - } - ex := CacheOptionsEntry{ - Type: "s3", - Attrs: map[string]string{ - "region": opts.Region, - "access_key_id": opts.AccessKeyID, - "secret_access_key": opts.SecretAccessKey, - "bucket": s3Bucket, - "endpoint_url": s3Addr, - "use_path_style": "true", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testBasicAzblobCacheImportExport(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendAzblob, - ) - - opts := helpers.AzuriteOpts{ - AccountName: "azblobcacheaccount", - AccountKey: base64.StdEncoding.EncodeToString([]byte("azblobcacheaccountkey")), - } - - azAddr, cleanup, err := helpers.NewAzuriteServer(t, sb, opts) - require.NoError(t, err) - defer cleanup() - - im := CacheOptionsEntry{ - Type: "azblob", - Attrs: map[string]string{ - "account_url": azAddr, - "account_name": opts.AccountName, - "secret_access_key": opts.AccountKey, - "container": "cachecontainer", - }, - } - ex := CacheOptionsEntry{ - Type: "azblob", - Attrs: map[string]string{ - "account_url": azAddr, - "account_name": opts.AccountName, - "secret_access_key": opts.AccountKey, - "container": "cachecontainer", - }, - } - testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex}) -} - -func testBasicInlineCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureDirectPush, - workers.FeatureCacheExport, - workers.FeatureCacheBackendInline, - ) - 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() - - 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) - } - - run(integration.UnixOrWindows( - `sh -c "echo -n foobar > const"`, - `cmd /C echo foobar > const`, - )) - run(integration.UnixOrWindows( - `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, - `cmd /C echo %RANDOM% > unique`, - )) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - target := registry + "/buildkit/testexportinline:latest" - - resp, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - }, nil) - require.NoError(t, err) - - dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureCacheBackendRegistry) - - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - // specifying inline cache exporter is needed for reproducing containerimage.digest - // (not needed for reproducing rootfs/unique) - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - require.Equal(t, dgst, dgst2) - - ensurePruneAll(t, c, sb) - - // Export the cache again with compression - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - // specifying inline cache exporter is needed for reproducing containerimage.digest - // (not needed for reproducing rootfs/unique) - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "compression": "uncompressed", // inline cache should work with compression - "force-compression": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst2uncompress, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - // dgst2uncompress != dgst, because the compression type is different - unique2uncompress, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2uncompress, "/unique") - require.NoError(t, err) - require.Equal(t, unique, unique2uncompress) - - ensurePruneAll(t, c, sb) - - resp, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - }, - }, - }, nil) - require.NoError(t, err) - - dgst3, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, true, ok) - - // dgst3 != dgst, because inline cache is not exported for dgst3 - unique3, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst3, "/unique") - require.NoError(t, err) - require.Equal(t, unique, unique3) -} - -func testRegistryEmptyCacheExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheBackendRegistry, - ) - - for _, ociMediaTypes := range []bool{true, false} { - for _, imageManifest := range []bool{true, false} { - if imageManifest && !ociMediaTypes { - // invalid configuration for remote cache - continue - } - - t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t/imageManifest=%t", ociMediaTypes, imageManifest), func(t *testing.T) { - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - st := llb.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) - - cacheTarget := registry + "/buildkit/testregistryemptycache:latest" - - cacheOptionsEntry := CacheOptionsEntry{ - Type: "registry", - Attrs: map[string]string{ - "ref": cacheTarget, - "image-manifest": strconv.FormatBool(imageManifest), - "oci-mediatypes": strconv.FormatBool(ociMediaTypes), - }, - } - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - CacheExports: []CacheOptionsEntry{cacheOptionsEntry}, - }, nil) - require.NoError(t, err) - - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - cdAddress := sb.ContainerdAddress() - var client *ctd.Client - if cdAddress != "" { - client, err = newContainerd(cdAddress) - require.NoError(t, err) - defer client.Close() - - _, err := client.Fetch(ctx, cacheTarget) - require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err) - } - }) - } - } -} - -func testMultipleRecordsWithSameLayersCacheImportExport(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, - workers.FeatureCacheExport, - workers.FeatureCacheImport, - workers.FeatureCacheBackendRegistry, - ) - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - target := registry + "/buildkit/testexport:latest" - cacheOpts := []CacheOptionsEntry{{ - Type: "registry", - Attrs: map[string]string{ - "ref": target, - }, - }} - - requiresLinux(t) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - base := llb.Image("busybox:latest") - // layerA and layerB create identical layers with different LLB - layerA := base.Run(llb.Args([]string{ - "sh", "-c", - `echo $(( 1 + 2 )) > /result && touch -d "1970-01-01 00:00:00" /result`, - })).Root() - layerB := base.Run(llb.Args([]string{ - "sh", "-c", - `echo $(( 2 + 1 )) > /result && touch -d "1970-01-01 00:00:00" /result`, - })).Root() - - combined := llb.Merge([]llb.State{layerA, layerB}) - combinedDef, err := combined.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), combinedDef, SolveOpt{ - CacheExports: cacheOpts, - }, nil) - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - - singleDef, err := layerA.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), singleDef, SolveOpt{ - CacheImports: cacheOpts, - }, nil) - require.NoError(t, err) - - // Ensure that even though layerA and layerB were both loaded as possible results - // and only was used, all the cache refs are released - // More context: https://github.com/moby/buildkit/pull/3815 - ensurePruneAll(t, c, sb) -} - -// testSnapshotWithMultipleBlobs verifies that BuildKit handles a single snapshot with multiple -// compressed blob representations. It pushes the same layer with compression levels 0 and 9, -// producing different blob digests, then pulls and exports both images to ensure no conflicts. -// moby/buildkit#3809 -func testSnapshotWithMultipleBlobs(t *testing.T, sb integration.Sandbox) { - // TODO: Re-enable on Windows once the HCS layer activation race is fixed upstream. - // Passes locally but fails on GitHub Actions with: mkdir ...\ProgramData\Microsoft: - // Cannot create a file when that file already exists. The bug is in the Windows - // snapshotter/differ layer activation path, not in this test. - integration.SkipOnPlatform(t, "windows", "HCS layer activation race: concurrent mkdir fails on CI (passes locally)") - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - // build two images with same layer but different compressed blobs - - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - now := time.Now() - - imgName := integration.UnixOrWindows("alpine", "nanoserver") - // On Windows: - // - llb.Scratch() doesn't work (Windows snapshotter requires a base OS layer) - // - llb.Copy across images doesn't work (dual mount: "number of mounts should always be 1") - // So we use llb.Image(nanoserver).File(llb.Mkfile(...)) which only needs one mount. - // The nanoserver base layers appear before our layer, so the final assertion - // compares a layer index that skips the base layers. - st := integration.UnixOrWindows( - llb.Scratch().File(llb.Copy(llb.Image(imgName), "/", "/"+imgName+"/", llb.WithCreatedTime(now))), - llb.Image(imgName).File(llb.Mkfile("/testdata", 0600, []byte("snapshot-test-content"), llb.WithCreatedTime(now))), - ) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - name1 := registry + "/multiblobtest1/image:latest" - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": name1, - "push": "true", - "compression-level": "0", - }, - }, - }, - }, nil) - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - - st = st.File(llb.Mkfile("test", 0600, []byte("test"))) // extra layer so we don't get a cache match based on image config rootfs only - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - name2 := registry + "/multiblobtest2/image:latest" - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: "image", - Attrs: map[string]string{ - "name": name2, - "push": "true", - "compression-level": "9", - }, - }, - }, - }, nil) - require.NoError(t, err) - - ensurePruneAll(t, c, sb) - - // first build with first image - destDir := t.TempDir() - - out1 := filepath.Join(destDir, "out1.tar") - outW1, err := os.Create(out1) - require.NoError(t, err) - - st = llb.Image(name1).File(llb.Mkfile("test", 0600, []byte("test1"))) - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterOCI, // make sure to export so blobs need to be loaded - Output: fixedWriteCloser(outW1), - }, - }, - }, nil) - require.NoError(t, err) - - // make sure second image does not cause any errors - out2 := filepath.Join(destDir, "out2.tar") - outW2, err := os.Create(out2) - require.NoError(t, err) - - st = llb.Image(name2).File(llb.Mkfile("test", 0600, []byte("test2"))) - - def, err = st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - FrontendAttrs: integration.UnixOrWindows( - map[string]string{"attest:sbom": ""}, - map[string]string{}, - ), - Exports: []ExportEntry{ - { - Type: ExporterOCI, // make sure to export so blobs need to be loaded - Output: fixedWriteCloser(outW2), - }, - }, - }, nil) - require.NoError(t, err) - - // extra validation that we did get different layer blobs - dt, err := os.ReadFile(out1) - 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) - - dt, err = os.ReadFile(out2) - require.NoError(t, err) - - m, err = testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index) - require.NoError(t, err) - - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &index) - require.NoError(t, err) - - var mfst2 ocispecs.Manifest - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst2) - require.NoError(t, err) - - // On Linux, Layers[0] is the layer we created (copy from alpine). - // On Windows, the nanoserver base layer(s) come first, so our layer is at a later index. - // We find the first layer that differs between the two manifests — that's the layer - // pushed with different compression levels. - layerIdx := integration.UnixOrWindows(0, len(mfst.Layers)-2) - require.NotEqual(t, mfst.Layers[layerIdx].Digest, mfst2.Layers[layerIdx].Digest) -} - -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 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 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 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 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 testExportLocalModeCopyMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox) { - testExportLocalModeMultiPlatformKeepsAllPlatforms(t, sb, false) -} - -func testExportLocalModeDeleteMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox) { - testExportLocalModeMultiPlatformKeepsAllPlatforms(t, sb, true) -} - -func testExportLocalModeMultiPlatformKeepsAllPlatforms(t *testing.T, sb integration.Sandbox, deleteMode bool) { - workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureMultiPlatform) - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - const filesPerPlatform = 20 - platformsToTest := []string{"linux/amd64", "linux/arm64", "linux/arm/v7", "linux/s390x"} - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{ - Platforms: make([]exptypes.Platform, len(platformsToTest)), - } - for i, platform := range platformsToTest { - st := llb.Scratch() - for j := range filesPerPlatform { - st = st.File( - llb.Mkfile(fmt.Sprintf("file-%03d.txt", j), 0600, fmt.Appendf(nil, "%s-%d", platform, j)), - ) - } - - def, err := st.Marshal(ctx) - if err != nil { - return nil, err - } - - r, err := c.Solve(ctx, gateway.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - return nil, err - } - - ref, err := r.SingleRef() - if err != nil { - return nil, err - } - - _, err = ref.ToState() - if err != nil { - return nil, err - } - res.AddRef(platform, ref) - - expPlatforms.Platforms[i] = exptypes.Platform{ - ID: platform, - Platform: platforms.MustParse(platform), - } - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - destDir := t.TempDir() - - // Pre-populate dest with directories matching the platform-split naming - // convention. Delete mode must remove stale files from all platform - // directories while preserving every platform's fresh output. - err = os.WriteFile(filepath.Join(destDir, "stale.txt"), []byte("stale"), 0600) - require.NoError(t, err) - for _, platform := range platformsToTest { - platDir := filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_")) - err = os.MkdirAll(platDir, 0755) - require.NoError(t, err) - for j := range filesPerPlatform { - err = os.WriteFile(filepath.Join(platDir, fmt.Sprintf("old-%03d.txt", j)), []byte("old"), 0600) - require.NoError(t, err) - } - } - - attrs := map[string]string{} - if deleteMode { - attrs["mode"] = "delete" - } - - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destDir, - Attrs: attrs, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - if deleteMode { - // Stale top-level file must be gone. - _, err = os.Stat(filepath.Join(destDir, "stale.txt")) - require.ErrorIs(t, err, os.ErrNotExist) - } - - // Every platform's build output must survive (no cross-platform deletion). - for _, platform := range platformsToTest { - platformDir := filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_")) - for j := range filesPerPlatform { - dt, err := os.ReadFile(filepath.Join(platformDir, fmt.Sprintf("file-%03d.txt", j))) - require.NoError(t, err, "missing build output file-%03d.txt for %s", j, platform) - require.Equal(t, fmt.Sprintf("%s-%d", platform, j), string(dt)) - } - if deleteMode { - // Pre-existing files within each platform dir must be cleaned up. - _, err = os.Stat(filepath.Join(platformDir, "old-000.txt")) - require.ErrorIs(t, err, os.ErrNotExist, "stale file not removed for %s", platform) - } - } -} - -func 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 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))) -} - -func testCachedMounts(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)) - } - - busybox := llb.Image(imgName) - // setup base for one of the cache sources - cmdPrefix := integration.UnixOrWindows( - `sh -c "echo -n`, - `cmd /C "echo`, - ) - st := busybox.Run(llb.Shlexf(`%s base > baz"`, cmdPrefix), llb.Dir("/wd")) - base := st.AddMount("/wd", scratch()) - - st = busybox.Run(llb.Shlexf(`%s first > foo"`, cmdPrefix), llb.Dir("/wd")) - - st.AddMount("/wd", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - cmdStr := integration.UnixOrWindows( - `sh -c "cat foo && echo -n second > /wd2/bar"`, - `cmd /C type foo && echo second > C:\\wd2\\bar`, - ) - st = st.Run(llb.Shlex(cmdStr), llb.Dir("/wd")) - st.AddMount("/wd", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - st.AddMount("/wd2", base, llb.AsPersistentCacheDir("mycache2", llb.CacheMountShared)) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - // repeat to make sure cache works - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - // second build using cache directories - cmdStr = integration.UnixOrWindows( - `sh -c "cp /src0/foo . && cp /src1/bar . && cp /src1/baz ."`, - `cmd /C copy C:\\src0\\foo . && copy C:\\src1\\bar . && copy C:\\src1\\baz .`, - ) - st = busybox.Run(llb.Shlex(cmdStr), llb.Dir("/wd")) - out := st.AddMount("/wd", scratch()) - st.AddMount("/src0", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - st.AddMount("/src1", base, llb.AsPersistentCacheDir("mycache2", llb.CacheMountShared)) - - destDir := t.TempDir() - - def, err = out.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) - - newLine := integration.UnixOrWindows("", " \r\n") - dt, err := os.ReadFile(filepath.Join(destDir, "foo")) - require.NoError(t, err) - require.Equal(t, "first"+newLine, string(dt)) - - dt, err = os.ReadFile(filepath.Join(destDir, "bar")) - require.NoError(t, err) - require.Equal(t, "second"+newLine, string(dt)) - - dt, err = os.ReadFile(filepath.Join(destDir, "baz")) - require.NoError(t, err) - require.Equal(t, "base"+newLine, string(dt)) - - checkAllReleasable(t, c, sb, true) -} - -func testSharedCacheMounts(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; while [[ ! -f two ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) - st.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - - st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; while [[ ! -f one ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) - st2.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - - out := busybox.Run(llb.Shlex("true")) - out.AddMount("/m1", st.Root()) - out.AddMount("/m2", st2.Root()) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -// #2334 -func testSharedCacheMountsNoScratch(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; while [[ ! -f two ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) - st.AddMount("/wd", llb.Image("busybox:latest"), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - - st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; while [[ ! -f one ]]; do ls -l; usleep 500000; done"`), llb.Dir("/wd")) - st2.AddMount("/wd", llb.Image("busybox:latest"), llb.AsPersistentCacheDir("mycache1", llb.CacheMountShared)) - - out := busybox.Run(llb.Shlex("true")) - out.AddMount("/m1", st.Root()) - out.AddMount("/m2", st2.Root()) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -func testLockedCacheMounts(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 := busybox.Run(llb.Shlex(`sh -e -c "touch one; if [[ -f two ]]; then exit 0; fi; for i in $(seq 10); do if [[ -f two ]]; then exit 1; fi; usleep 200000; done"`), llb.Dir("/wd")) - st.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - - st2 := busybox.Run(llb.Shlex(`sh -e -c "touch two; if [[ -f one ]]; then exit 0; fi; for i in $(seq 10); do if [[ -f one ]]; then exit 1; fi; usleep 200000; done"`), llb.Dir("/wd")) - st2.AddMount("/wd", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - - out := busybox.Run(llb.Shlex("true")) - out.AddMount("/m1", st.Root()) - out.AddMount("/m2", st2.Root()) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -func testDuplicateCacheMount(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) - scratch := func() llb.State { - return integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName)) - } - - cmdStr := integration.UnixOrWindows( - `sh -e -c "[[ ! -f /m2/foo ]]; touch /m1/foo; [[ -f /m2/foo ]];"`, - `cmd /C echo a > \\m1\\foo && dir \\m2\\foo`, - ) - out := busybox.Run(llb.Shlex(cmdStr)) - out.AddMount("/m1", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - out.AddMount("/m2", scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -func testRunCacheWithMounts(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) - - imgAlpine := integration.UnixOrWindows("alpine:latest", "nanoserver:plus") - alpineImage := llb.Image(imgAlpine) - - // On Windows ARM64, nanoserver:plus and nanoserver:latest map to the same - // image, so we create a marker file to distinguish the mounted image. - mountSource := alpineImage - if runtime.GOOS == "windows" { - mountSource = alpineImage.Run( - llb.Shlex(`cmd /C echo 1> C:/marker`), - ).Root() - } - - cmdStr := integration.UnixOrWindows( - `sh -e -c "[[ -f /m1/sbin/apk ]]"`, - `cmd /C if exist C:/m1/marker (exit 0) else (exit 1)`, - ) - out := busybox.Run(llb.Shlex(cmdStr)) - out.AddMount("/m1", mountSource, llb.Readonly) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - cmdStr = integration.UnixOrWindows( - `sh -e -c "[[ ! -f /m1/sbin/apk ]]"`, - `cmd /C if exist C:/m1/marker (exit 1) else (exit 0)`, - ) - out = busybox.Run(llb.Shlex(cmdStr)) - out.AddMount("/m1", llb.Image(imgName), llb.Readonly) - - def, err = out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -func testCacheMountNoCache(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") - - out := busybox.Run(llb.Shlex(`sh -e -c "touch /m1/foo; touch /m2/bar"`)) - out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - out.AddMount("/m2", llb.Scratch(), llb.AsPersistentCacheDir("mycache2", llb.CacheMountLocked)) - - def, err := out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - out = busybox.Run(llb.Shlex(`sh -e -c "[[ ! -f /m1/foo ]]; touch /m1/foo2;"`), llb.IgnoreCache) - out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - - def, err = out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - out = busybox.Run(llb.Shlex(`sh -e -c "[[ -f /m1/foo2 ]]; [[ -f /m2/bar ]];"`)) - out.AddMount("/m1", llb.Scratch(), llb.AsPersistentCacheDir("mycache1", llb.CacheMountLocked)) - out.AddMount("/m2", llb.Scratch(), llb.AsPersistentCacheDir("mycache2", llb.CacheMountLocked)) - - def, err = out.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) -} - -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) -} - -// #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) -} - -// #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) - } -} - -// #319 -func testMountWithNoSource(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)) - - var nilState llb.State - - // This should never actually be run, but we want to succeed - // if it was, because we expect an error below, or a daemon - // panic if the issue has regressed. - cmdArgs := integration.UnixOrWindows( - []string{"/bin/true"}, - []string{"cmd", "/C", "exit 0"}, - ) - run := busybox.Run( - llb.Args(cmdArgs), - llb.AddMount("/nil", nilState, llb.SourcePath("/"), llb.Readonly)) - - st = run.AddMount("/mnt", st) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) -} - -// #324 -func testReadonlyRootFS(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("docker.io/library/busybox:latest") - st := llb.Scratch() - - // The path /foo should be unwriteable. - run := busybox.Run( - llb.ReadonlyRootFS(), - llb.Args([]string{"/bin/touch", "/foo"})) - st = run.AddMount("/mnt", st) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(sb.Context(), def, SolveOpt{}, nil) - require.Error(t, err) - // Would prefer to detect more specifically "Read-only file - // system" but that isn't exposed here (it is on the stdio - // which we don't see). - require.Contains(t, err.Error(), "process \"/bin/touch /foo\" did not complete successfully") - - checkAllReleasable(t, c, sb, true) -} - -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) -} - -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))) -} - -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 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 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) - }) -} - -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 -} - -func cacheMountFile(base llb.State, cacheID, name string) llb.State { - check := base.Run(llb.Shlexf(`sh -e -c "echo %s >/dev/null; cp /cache/%s /out/%s"`, cacheID, name, name)) - check.AddMount("/cache", llb.Scratch(), llb.AsPersistentCacheDir(cacheID, llb.CacheMountShared)) - return check.AddMount("/out", llb.Scratch()) -} - -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 testMergeOpCacheInline(t *testing.T, sb integration.Sandbox) { - testMergeOpCache(t, sb, "inline") -} - -func testMergeOpCacheMin(t *testing.T, sb integration.Sandbox) { - testMergeOpCache(t, sb, "min") -} - -func testMergeOpCacheMax(t *testing.T, sb integration.Sandbox) { - testMergeOpCache(t, sb, "max") -} - -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 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 runShellExecState(base llb.State, cmds ...string) llb.ExecState { - return base.Run(llb.Args([]string{"sh", "-c", strings.Join(cmds, " && ")})) -} - -func runShell(base llb.State, cmds ...string) llb.State { - return runShellExecState(base, cmds...).Root() -} - -func chainRunShells(base llb.State, cmdss ...[]string) llb.State { - for _, cmds := range cmdss { - base = runShell(base, cmds...) - } - return base -} - -func requiresLinux(t *testing.T) { - integration.SkipOnPlatform(t, "!linux") -} - -// 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 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 testInvalidExporter(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") - def, err := llb.Image(imgName).Marshal(sb.Context()) - require.NoError(t, err) - - destDir := t.TempDir() - - target := "example.com/buildkit/testoci:latest" - attrs := map[string]string{ - "name": target, - } - for _, exp := range []string{ExporterOCI, ExporterDocker} { - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: exp, - Attrs: attrs, - }, - }, - }, nil) - // output file writer is required - require.Error(t, err) - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: exp, - Attrs: attrs, - OutputDir: destDir, - }, - }, - }, nil) - // output directory is not supported - require.Error(t, err) - } - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - Attrs: attrs, - }, - }, - }, nil) - // output directory is required - require.Error(t, err) - - f, err := os.Create(filepath.Join(destDir, "a")) - require.NoError(t, err) - defer f.Close() - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - Attrs: attrs, - Output: fixedWriteCloser(f), - }, - }, - }, nil) - // output file writer is not supported - require.Error(t, err) - - checkAllReleasable(t, c, sb, true) -} - -// 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) -} - -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) -} - -// 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 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 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 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) -} - -func testExportAnnotations(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() - - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) - - amd64 := platforms.MustParse("linux/amd64") - arm64 := platforms.MustParse("linux/arm64") - ps := []ocispecs.Platform{amd64, arm64} - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{ - Platforms: make([]exptypes.Platform, len(ps)), - } - for i, p := range ps { - st := scratch.File( - llb.Mkfile("platform", 0600, []byte(platforms.Format(p))), - ) - - 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 - } - - k := platforms.Format(p) - res.AddRef(k, ref) - - expPlatforms.Platforms[i] = exptypes.Platform{ - ID: k, - Platform: p, - } - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - res.AddMeta(exptypes.AnnotationIndexKey("gi"), []byte("generic index")) - res.AddMeta(exptypes.AnnotationIndexDescriptorKey("gid"), []byte("generic index descriptor")) - res.AddMeta(exptypes.AnnotationManifestKey(nil, "gm"), []byte("generic manifest")) - res.AddMeta(exptypes.AnnotationManifestDescriptorKey(nil, "gmd"), []byte("generic manifest descriptor")) - res.AddMeta(exptypes.AnnotationManifestKey(&amd64, "m"), []byte("amd64 manifest")) - res.AddMeta(exptypes.AnnotationManifestKey(&arm64, "m"), []byte("arm64 manifest")) - res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&amd64, "md"), []byte("amd64 manifest descriptor")) - res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&arm64, "md"), []byte("arm64 manifest descriptor")) - res.AddMeta(exptypes.AnnotationKey{Key: "gd"}.String(), []byte("generic default")) - - return res, nil - } - - // testing for image exporter - - target := registry + "/buildkit/testannotations:latest" - - const created = "2022-01-23T12:34:56Z" - - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "annotation-index.gio": "generic index opt", - "annotation-index." + ocispecs.AnnotationCreated: created, - "annotation-manifest.gmo": "generic manifest opt", - "annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt", - "annotation-manifest[linux/amd64].mo": "amd64 manifest opt", - "annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt", - "annotation-manifest[linux/arm64].mo": "arm64 manifest opt", - "annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - require.Equal(t, "generic index", imgs.Index.Annotations["gi"]) - require.Equal(t, "generic index opt", imgs.Index.Annotations["gio"]) - require.Equal(t, created, imgs.Index.Annotations[ocispecs.AnnotationCreated]) - for _, desc := range imgs.Index.Manifests { - require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"]) - require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"]) - switch { - case platforms.Only(amd64).Match(*desc.Platform): - require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"]) - require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"]) - case platforms.Only(arm64).Match(*desc.Platform): - require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"]) - require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"]) - default: - require.Fail(t, "unrecognized platform") - } - } - - amdImage := imgs.Find(platforms.Format(amd64)) - require.Equal(t, "generic default", amdImage.Manifest.Annotations["gd"]) - require.Equal(t, "generic manifest", amdImage.Manifest.Annotations["gm"]) - require.Equal(t, "generic manifest opt", amdImage.Manifest.Annotations["gmo"]) - require.Equal(t, "amd64 manifest", amdImage.Manifest.Annotations["m"]) - require.Equal(t, "amd64 manifest opt", amdImage.Manifest.Annotations["mo"]) - - armImage := imgs.Find(platforms.Format(arm64)) - require.Equal(t, "generic default", armImage.Manifest.Annotations["gd"]) - require.Equal(t, "generic manifest", armImage.Manifest.Annotations["gm"]) - require.Equal(t, "generic manifest opt", armImage.Manifest.Annotations["gmo"]) - require.Equal(t, "arm64 manifest", armImage.Manifest.Annotations["m"]) - require.Equal(t, "arm64 manifest opt", armImage.Manifest.Annotations["mo"]) - - // testing for oci exporter - - destDir := t.TempDir() - - out := filepath.Join(destDir, "out.tar") - outW, err := os.Create(out) - require.NoError(t, err) - - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterOCI, - Output: fixedWriteCloser(outW), - Attrs: map[string]string{ - "annotation-index.gio": "generic index opt", - "annotation-index-descriptor.gido": "generic index descriptor opt", - "annotation-index-descriptor." + ocispecs.AnnotationCreated: created, - "annotation-manifest.gmo": "generic manifest opt", - "annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt", - "annotation-manifest[linux/amd64].mo": "amd64 manifest opt", - "annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt", - "annotation-manifest[linux/arm64].mo": "arm64 manifest opt", - "annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt", - }, - }, - }, - }, "", frontend, 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 layout ocispecs.Index - err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &layout) - require.Equal(t, "generic index descriptor", layout.Manifests[0].Annotations["gid"]) - require.Equal(t, "generic index descriptor opt", layout.Manifests[0].Annotations["gido"]) - require.Equal(t, created, layout.Manifests[0].Annotations[ocispecs.AnnotationCreated]) - require.NoError(t, err) - - var index ocispecs.Index - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+layout.Manifests[0].Digest.Hex()].Data, &index) - require.Equal(t, "generic index", index.Annotations["gi"]) - require.Equal(t, "generic index opt", index.Annotations["gio"]) - require.NoError(t, err) - - for _, desc := range index.Manifests { - var mfst ocispecs.Manifest - err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+desc.Digest.Hex()].Data, &mfst) - require.NoError(t, err) - - require.Equal(t, "generic default", mfst.Annotations["gd"]) - require.Equal(t, "generic manifest", mfst.Annotations["gm"]) - require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"]) - require.Equal(t, "generic manifest opt", mfst.Annotations["gmo"]) - require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"]) - - switch { - case platforms.Only(amd64).Match(*desc.Platform): - require.Equal(t, "amd64 manifest", mfst.Annotations["m"]) - require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"]) - require.Equal(t, "amd64 manifest opt", mfst.Annotations["mo"]) - require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"]) - case platforms.Only(arm64).Match(*desc.Platform): - require.Equal(t, "arm64 manifest", mfst.Annotations["m"]) - require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"]) - require.Equal(t, "arm64 manifest opt", mfst.Annotations["mo"]) - require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"]) - default: - require.Fail(t, "unrecognized platform") - } - } -} - -func testExportAnnotationsMediaTypes(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) - - p := platforms.DefaultSpec() - ps := []ocispecs.Platform{p} - - scratch := integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{ - Platforms: make([]exptypes.Platform, len(ps)), - } - for i, p := range ps { - st := scratch.File( - llb.Mkfile("platform", 0600, []byte(platforms.Format(p))), - ) - - 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 - } - - k := platforms.Format(p) - res.AddRef(k, ref) - - expPlatforms.Platforms[i] = exptypes.Platform{ - ID: k, - Platform: p, - } - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - target := registry + "/buildkit/testannotationsmedia:1" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - "annotation-manifest.a": "b", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 1, len(imgs.Images)) - - target2 := registry + "/buildkit/testannotationsmedia:2" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target2, - "push": "true", - "annotation-index.c": "d", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target2) - require.NoError(t, err) - imgs2, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 1, len(imgs2.Images)) - - require.Equal(t, "b", imgs.Images[0].Manifest.Annotations["a"]) - require.Equal(t, "d", imgs2.Index.Annotations["c"]) - - require.Equal(t, ocispecs.MediaTypeImageIndex, imgs.Index.MediaType) - require.Equal(t, ocispecs.MediaTypeImageIndex, imgs2.Index.MediaType) -} - -func testExportAttestationsDefaultOCIArtifact(t *testing.T, sb integration.Sandbox) { - testExportAttestations(t, sb, false, false) -} - -func testExportAttestationsOCIArtifact(t *testing.T, sb integration.Sandbox) { - testExportAttestations(t, sb, true, true) -} - -func testExportAttestationsImageManifest(t *testing.T, sb integration.Sandbox) { - testExportAttestations(t, sb, false, true) -} - -func testExportAttestations(t *testing.T, sb integration.Sandbox, ociArtifact bool, setOCIArtifact bool) { - 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) - - ps := []ocispecs.Platform{ - platforms.MustParse("linux/amd64"), - platforms.MustParse("linux/arm64"), - } - - success := []byte(`{"success": true}`) - successDigest := digest.SHA256.FromBytes(success) - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{} - - for _, p := range ps { - pk := platforms.Format(p) - expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) - - // build image - st := llb.Scratch().File( - llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), - ) - 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(pk, ref) - - // build attestations - st = llb.Scratch(). - File(llb.Mkfile("/attestation.json", 0600, success)). - File(llb.Mkfile("/attestation2.json", 0600, []byte{})) - 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 - } - refAttest, err := r.SingleRef() - if err != nil { - return nil, err - } - _, err = ref.ToState() - if err != nil { - return nil, err - } - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_InToto, - Ref: refAttest, - Path: "/attestation.json", - InToto: result.InTotoAttestation{ - PredicateType: "https://example.com/attestations/v1.0", - Subjects: []result.InTotoSubject{{ - Kind: gatewaypb.InTotoSubjectKind_Self, - }}, - }, - }) - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_InToto, - Ref: refAttest, - Path: "/attestation2.json", - InToto: result.InTotoAttestation{ - PredicateType: "https://example.com/attestations2/v1.0", - Subjects: []result.InTotoSubject{{ - Kind: gatewaypb.InTotoSubjectKind_Raw, - Name: "/attestation.json", - Digest: []digest.Digest{successDigest}, - }}, - }, - }) - } - - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - t.Run("image", func(t *testing.T) { - expectedOCIArtifact := !setOCIArtifact || ociArtifact - targets := []string{ - registry + "/buildkit/testattestationsfoo:latest", - registry + "/buildkit/testattestationsbar:latest", - } - attrs := map[string]string{ - "name": strings.Join(targets, ","), - "push": "true", - } - if setOCIArtifact { - attrs["oci-artifact"] = strconv.FormatBool(ociArtifact) - } - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: attrs, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(targets[0]) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, len(ps)*2, len(imgs.Images)) - - var bases []*testutil.ImageInfo - for _, p := range ps { - pk := platforms.Format(p) - img := imgs.Find(pk) - require.NotNil(t, img) - require.Equal(t, pk, platforms.Format(*img.Desc.Platform)) - require.Equal(t, 1, len(img.Layers)) - require.Equal(t, fmt.Appendf(nil, "hello %s!", pk), img.Layers[0]["greeting"].Data) - bases = append(bases, img) - } - - atts := imgs.Filter("unknown/unknown") - require.Equal(t, len(ps), len(atts.Images)) - for i, att := range atts.Images { - require.Equal(t, ocispecs.MediaTypeImageManifest, att.Desc.MediaType) - require.Equal(t, "unknown/unknown", platforms.Format(*att.Desc.Platform)) - require.Equal(t, attestation.DockerAnnotationReferenceTypeDefault, att.Desc.Annotations[attestation.DockerAnnotationReferenceType]) - require.Equal(t, bases[i].Desc.Digest.String(), att.Desc.Annotations[attestation.DockerAnnotationReferenceDigest]) - require.Equal(t, 2, len(att.Layers)) - - if expectedOCIArtifact { - subject := att.Manifest.Subject - require.NotNil(t, subject) - require.Equal(t, bases[i].Desc.MediaType, subject.MediaType) - require.Equal(t, bases[i].Desc.Digest, subject.Digest) - require.Equal(t, bases[i].Desc.Size, subject.Size) - require.Empty(t, subject.Annotations) - require.Nil(t, subject.Platform) - require.Equal(t, "application/vnd.docker.attestation.manifest.v1+json", att.Manifest.ArtifactType) - require.Equal(t, ocispecs.DescriptorEmptyJSON, att.Manifest.Config) - } else { - require.Nil(t, att.Manifest.Subject) - require.Empty(t, att.Manifest.ArtifactType) - - // image config is not included in the OCI artifact - require.Equal(t, "unknown/unknown", att.Img.OS+"/"+att.Img.Architecture) - require.Equal(t, len(att.Layers), len(att.Img.RootFS.DiffIDs)) - require.Equal(t, 0, len(att.Img.History)) - } - - var attest intoto.Statement - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - - purls := map[string]string{} - for _, k := range targets { - named, err := reference.ParseNormalizedNamed(k) - require.NoError(t, err) - name := reference.FamiliarName(named) - version := "" - if tagged, ok := named.(reference.Tagged); ok { - version = tagged.Tag() - } - p := fmt.Sprintf("pkg:docker/%s%s@%s?platform=%s", url.QueryEscape(registry), strings.TrimPrefix(name, registry), version, url.PathEscape(platforms.Format(ps[i]))) - purls[k] = p - } - - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) - require.Equal(t, map[string]any{"success": true}, attest.Predicate) - subjects := []intoto.Subject{ - { - Name: purls[targets[0]], - Digest: map[string]string{ - "sha256": bases[i].Desc.Digest.Encoded(), - }, - }, - { - Name: purls[targets[1]], - Digest: map[string]string{ - "sha256": bases[i].Desc.Digest.Encoded(), - }, - }, - } - require.Equal(t, subjects, attest.Subject) - - var attest2 intoto.Statement - require.NoError(t, json.Unmarshal(att.LayersRaw[1], &attest2)) - - require.Equal(t, intoto.StatementInTotoV1, attest2.Type) - require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) - require.Nil(t, attest2.Predicate) - subjects = []intoto.Subject{{ - Name: "/attestation.json", - Digest: map[string]string{ - "sha256": successDigest.Encoded(), - }, - }} - require.Equal(t, subjects, attest2.Subject) - } - - cdAddress := sb.ContainerdAddress() - if cdAddress == "" { - return - } - client, err := ctd.New(cdAddress) - require.NoError(t, err) - defer client.Close() - ctx := namespaces.WithNamespace(sb.Context(), "buildkit") - - for _, target := range targets { - err = client.ImageService().Delete(ctx, target, images.SynchronousDelete()) - require.NoError(t, err) - } - checkAllReleasable(t, c, sb, true) - }) - - t.Run("local", func(t *testing.T) { - dir := t.TempDir() - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: dir, - Attrs: map[string]string{ - "attestation-prefix": "test.", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - for _, p := range ps { - var attest intoto.Statement - dt, err := os.ReadFile(path.Join(dir, strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(dt, &attest)) - - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) - require.Equal(t, map[string]any{"success": true}, attest.Predicate) - - require.Equal(t, []intoto.Subject{{ - Name: "greeting", - Digest: result.ToDigestMap(digest.Canonical.FromString("hello " + platforms.Format(p) + "!")), - }}, attest.Subject) - - var attest2 intoto.Statement - dt, err = os.ReadFile(path.Join(dir, strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(dt, &attest2)) - - require.Equal(t, intoto.StatementInTotoV1, attest2.Type) - require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) - require.Nil(t, attest2.Predicate) - subjects := []intoto.Subject{{ - Name: "/attestation.json", - Digest: map[string]string{ - "sha256": successDigest.Encoded(), - }, - }} - require.Equal(t, subjects, attest2.Subject) - } - }) - - t.Run("tar", func(t *testing.T) { - dir := t.TempDir() - out := filepath.Join(dir, "out.tar") - outW, err := os.Create(out) - require.NoError(t, err) - - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterTar, - Output: fixedWriteCloser(outW), - Attrs: map[string]string{ - "attestation-prefix": "test.", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - dt, err := os.ReadFile(out) - require.NoError(t, err) - - m, err := testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - for _, p := range ps { - var attest intoto.Statement - item := m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation.json")] - require.NotNil(t, item) - require.NoError(t, json.Unmarshal(item.Data, &attest)) - - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) - require.Equal(t, map[string]any{"success": true}, attest.Predicate) - - require.Equal(t, []intoto.Subject{{ - Name: "greeting", - Digest: result.ToDigestMap(digest.Canonical.FromString("hello " + platforms.Format(p) + "!")), - }}, attest.Subject) - - var attest2 intoto.Statement - item = m[path.Join(strings.ReplaceAll(platforms.Format(p), "/", "_"), "test.attestation2.json")] - require.NotNil(t, item) - require.NoError(t, json.Unmarshal(item.Data, &attest2)) - - require.Equal(t, intoto.StatementInTotoV1, attest2.Type) - require.Equal(t, "https://example.com/attestations2/v1.0", attest2.PredicateType) - require.Nil(t, attest2.Predicate) - subjects := []intoto.Subject{{ - Name: "/attestation.json", - Digest: map[string]string{ - "sha256": successDigest.Encoded(), - }, - }} - require.Equal(t, subjects, attest2.Subject) - } - }) -} - -func testAttestationDefaultSubject(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) - - ps := []ocispecs.Platform{ - platforms.MustParse("linux/amd64"), - } - - success := []byte(`{"success": true}`) - scratch := func() llb.State { - return integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) - } - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{} - - for _, p := range ps { - pk := platforms.Format(p) - expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) - - // build image - st := scratch().File( - llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), - ) - 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(pk, ref) - - // build attestations - st = scratch().File(llb.Mkfile("/attestation.json", 0600, success)) - 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 - } - refAttest, err := r.SingleRef() - if err != nil { - return nil, err - } - _, err = ref.ToState() - if err != nil { - return nil, err - } - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_InToto, - Ref: refAttest, - Path: "/attestation.json", - InToto: result.InTotoAttestation{ - PredicateType: "https://example.com/attestations/v1.0", - }, - }) - } - - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - target := registry + "/buildkit/testattestationsemptysubject:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, len(ps)*2, len(imgs.Images)) - - var bases []*testutil.ImageInfo - for _, p := range ps { - pk := platforms.Format(p) - bases = append(bases, imgs.Find(pk)) - } - - atts := imgs.Filter("unknown/unknown") - require.Equal(t, len(ps), len(atts.Images)) - for i, att := range atts.Images { - var attest intoto.Statement - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) - require.Equal(t, map[string]any{"success": true}, attest.Predicate) - - name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsemptysubject@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i]))) - subjects := []intoto.Subject{{ - Name: name, - Digest: map[string]string{ - "sha256": bases[i].Desc.Digest.Encoded(), - }, - }} - require.Equal(t, subjects, attest.Subject) - } -} - -func testAttestationBundle(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) - - ps := []ocispecs.Platform{ - platforms.MustParse("linux/amd64"), - } - - scratch := func() llb.State { - return integration.UnixOrWindows(llb.Scratch(), llb.Image("nanoserver")) - } - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - expPlatforms := &exptypes.Platforms{} - - for _, p := range ps { - pk := platforms.Format(p) - expPlatforms.Platforms = append(expPlatforms.Platforms, exptypes.Platform{ID: pk, Platform: p}) - - // build image - st := scratch().File( - llb.Mkfile("/greeting", 0600, fmt.Appendf(nil, "hello %s!", pk)), - ) - 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(pk, ref) - - stmt := intoto.Statement{ - StatementHeader: intoto.StatementHeader{ - Type: intoto.StatementInTotoV1, - PredicateType: "https://example.com/attestations/v1.0", - }, - Predicate: map[string]any{ - "foo": "1", - }, - } - buff := bytes.NewBuffer(nil) - enc := json.NewEncoder(buff) - require.NoError(t, enc.Encode(stmt)) - - // build attestations - st = scratch().File( - llb.Mkdir("/bundle", 0700), - ) - st = st.File( - llb.Mkfile("/bundle/attestation.json", 0600, buff.Bytes()), - ) - 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 - } - refAttest, err := r.SingleRef() - if err != nil { - return nil, err - } - _, err = ref.ToState() - if err != nil { - return nil, err - } - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_Bundle, - Ref: refAttest, - Path: "/bundle", - }) - } - - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - target := registry + "/buildkit/testattestationsbundle:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, len(ps)*2, len(imgs.Images)) - - var bases []*testutil.ImageInfo - for _, p := range ps { - pk := platforms.Format(p) - bases = append(bases, imgs.Find(pk)) - } - - atts := imgs.Filter("unknown/unknown") - require.Equal(t, len(ps)*1, len(atts.Images)) - for i, att := range atts.Images { - require.Equal(t, 1, len(att.LayersRaw)) - var attest intoto.Statement - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - - require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType) - require.Equal(t, map[string]any{"foo": "1"}, attest.Predicate) - name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsbundle@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i]))) - subjects := []intoto.Subject{{ - Name: name, - Digest: map[string]string{ - "sha256": bases[i].Desc.Digest.Encoded(), - }, - }} - require.Equal(t, subjects, attest.Subject) - } -} - -func testSBOMScan(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) - 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) - - p := platforms.MustParse("linux/amd64") - pk := platforms.Format(p) - - scannerFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - st := llb.Image("busybox", llb.Platform(p)) - def, err := st.Marshal(sb.Context()) - require.NoError(t, 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.SetRef(ref) - - var img ocispecs.Image - cmd := ` -cat < $BUILDKIT_SCAN_DESTINATION/spdx.json -{ - "_type": "https://in-toto.io/Statement/v1", - "predicateType": "https://spdx.dev/Document", - "predicate": { - "name": "fallback", - "extraParams": { - "ARG1": "$BUILDKIT_SCAN_ARG1", - "ARG2": "$BUILDKIT_SCAN_ARG2" - } - } -} -EOF -` - img.Config.Cmd = []string{"/bin/sh", "-c", cmd} - img.Platform = p - config, err := json.Marshal(img) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal image config") - } - res.AddMeta(exptypes.ExporterImageConfigKey, config) - - return res, nil - } - - scannerTarget := registry + "/buildkit/testsbomscanner:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": scannerTarget, - "push": "true", - "oci-mediatypes": "false", - }, - }, - }, - }, "", scannerFrontend, nil) - require.NoError(t, err) - - makeTargetFrontend := func(attest bool) func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - return func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - // build image - st := llb.Scratch().File( - llb.Mkfile("/greeting", 0600, []byte("hello world!")), - ) - 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(pk, ref) - - expPlatforms := &exptypes.Platforms{ - Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - // build attestations - if attest { - st = llb.Scratch(). - File(llb.Mkfile("/result.spdx", 0600, []byte(`{"name": "frontend"}`))) - 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 - } - refAttest, err := r.SingleRef() - if err != nil { - return nil, err - } - _, err = ref.ToState() - if err != nil { - return nil, err - } - - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_InToto, - Ref: refAttest, - Path: "/result.spdx", - InToto: result.InTotoAttestation{ - PredicateType: intoto.PredicateSPDX, - }, - }) - } - - return res, nil - } - } - - // test the default fallback scanner - target := registry + "/buildkit/testsbom:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(false), nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - // test the frontend builtin scanner - target = registry + "/buildkit/testsbom2:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(true), nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att := imgs.Find("unknown/unknown") - attest := intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{"name": "frontend"}) - - // test the specified fallback scanner - target = registry + "/buildkit/testsbom3:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "generator=" + scannerTarget, - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(false), nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att = imgs.Find("unknown/unknown") - attest = intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{"name": "fallback"}) - - // test the builtin frontend scanner and the specified fallback scanner together - target = registry + "/buildkit/testsbom3:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "generator=" + scannerTarget, - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(true), nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att = imgs.Find("unknown/unknown") - attest = intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{"name": "frontend"}) - - // test configuring the scanner (simple) - target = registry + "/buildkit/testsbom4:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "generator=" + scannerTarget + ",ARG1=foo,ARG2=bar", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(false), nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att = imgs.Find("unknown/unknown") - attest = intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{ - "extraParams": map[string]any{"ARG1": "foo", "ARG2": "bar"}, - }) - - // test configuring the scanner (complex) - target = registry + "/buildkit/testsbom4:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "\"generator=" + scannerTarget + "\",\"ARG1=foo\",\"ARG2=hello,world\"", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", makeTargetFrontend(false), nil) - require.NoError(t, err) - - desc, provider, err = contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err = testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att = imgs.Find("unknown/unknown") - attest = intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{ - "extraParams": map[string]any{"ARG1": "foo", "ARG2": "hello,world"}, - }) -} - -func testSBOMScanSingleRef(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) - 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) - - p := platforms.DefaultSpec() - pk := platforms.Format(p) - - scannerFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - st := llb.Image("busybox", llb.Platform(p)) - def, err := st.Marshal(sb.Context()) - require.NoError(t, 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.SetRef(ref) - - var img ocispecs.Image - cmd := ` -cat < $BUILDKIT_SCAN_DESTINATION/spdx.json -{ - "_type": "https://in-toto.io/Statement/v1", - "predicateType": "https://spdx.dev/Document", - "predicate": {"name": "fallback"} -} -EOF -` - img.Config.Cmd = []string{"/bin/sh", "-c", cmd} - img.Platform = p - config, err := json.Marshal(img) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal image config") - } - res.AddMeta(exptypes.ExporterImageConfigKey, config) - - return res, nil - } - - scannerTarget := registry + "/buildkit/testsbomscanner:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": scannerTarget, - "push": "true", - "oci-mediatypes": "false", - }, - }, - }, - }, "", scannerFrontend, nil) - require.NoError(t, err) - - targetFrontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - // build image - st := llb.Scratch().File( - llb.Mkfile("/greeting", 0600, []byte("hello world!")), - ) - 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.SetRef(ref) - - var img ocispecs.Image - img.Config.Cmd = []string{"/bin/sh", "-c", "cat /greeting"} - img.Platform = p - config, err := json.Marshal(img) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal image config") - } - res.AddMeta(exptypes.ExporterImageConfigKey, config) - - expPlatforms := &exptypes.Platforms{ - Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - target := registry + "/buildkit/testsbomsingle:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "generator=" + scannerTarget, - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", targetFrontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - var img *testutil.ImageInfo - for _, candidate := range imgs.Images { - if candidate.Desc.Platform != nil && platforms.Format(*candidate.Desc.Platform) == "unknown/unknown" { - continue - } - img = candidate - break - } - require.NotNil(t, img) - require.Equal(t, []string{"/bin/sh", "-c", "cat /greeting"}, img.Img.Config.Cmd) - - att := imgs.Find("unknown/unknown") - require.NotNil(t, att) - attest := intoto.Statement{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - require.Subset(t, attest.Predicate, map[string]any{"name": "fallback"}) -} - -func testSBOMSupplements(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureSBOM) - 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) - - p := platforms.MustParse("linux/amd64") - pk := platforms.Format(p) - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - // build image - st := llb.Scratch().File( - llb.Mkfile("/foo", 0600, []byte{}), - ) - 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(pk, ref) - - expPlatforms := &exptypes.Platforms{ - Platforms: []exptypes.Platform{{ID: pk, Platform: p}}, - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - // build attestations - doc := spdx.Document{ - SPDXVersion: "SPDX-2.2", - SPDXIdentifier: "DOCUMENT", - Files: []*spdx.File{ - { - // foo exists... - FileSPDXIdentifier: "SPDXRef-File-foo", - FileName: "/foo", - }, - { - // ...but bar doesn't - FileSPDXIdentifier: "SPDXRef-File-bar", - FileName: "/bar", - }, - }, - } - docBytes, err := json.Marshal(doc) - if err != nil { - return nil, err - } - st = llb.Scratch(). - File(llb.Mkfile("/result.spdx", 0600, docBytes)) - 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 - } - refAttest, err := r.SingleRef() - if err != nil { - return nil, err - } - _, err = ref.ToState() - if err != nil { - return nil, err - } - - res.AddAttestation(pk, gateway.Attestation{ - Kind: gatewaypb.AttestationKind_InToto, - Ref: refAttest, - Path: "/result.spdx", - InToto: result.InTotoAttestation{ - PredicateType: intoto.PredicateSPDX, - }, - Metadata: map[string][]byte{ - result.AttestationSBOMCore: []byte("result"), - }, - }) - - return res, nil - } - - // test the default fallback scanner - target := registry + "/buildkit/testsbom:latest" - _, err = c.Build(sb.Context(), SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:sbom": "", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - desc, provider, err := contentutil.ProviderFromRef(target) - require.NoError(t, err) - - imgs, err := testutil.ReadImages(sb.Context(), provider, desc) - require.NoError(t, err) - require.Equal(t, 2, len(imgs.Images)) - - att := imgs.Find("unknown/unknown") - attest := struct { - intoto.StatementHeader - Predicate spdx.Document - }{} - require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest)) - require.Equal(t, intoto.StatementInTotoV1, attest.Type) - require.Equal(t, intoto.PredicateSPDX, attest.PredicateType) - - require.Equal(t, "DOCUMENT", string(attest.Predicate.SPDXIdentifier)) - require.Len(t, attest.Predicate.Files, 2) - require.Equal(t, "/foo", attest.Predicate.Files[0].FileName) - require.Regexp(t, "^layerID: sha256:", attest.Predicate.Files[0].FileComment) - require.Equal(t, "/bar", attest.Predicate.Files[1].FileName) - require.Empty(t, attest.Predicate.Files[1].FileComment) - - checkAllReleasable(t, c, sb, true) -} - -func testMultipleCacheExports(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureMultiCacheExport) - 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) - - 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) - } - - constCmd := integration.UnixOrWindows( - `sh -c "echo -n foobar > const"`, - `cmd /C echo foobar > const`, - ) - uniqueCmd := integration.UnixOrWindows( - `sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`, - `cmd /C echo %RANDOM% > unique`, - ) - - run(constCmd) - run(uniqueCmd) - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - target := path.Join(registry, "image:test") - target2 := path.Join(registry, "image-copy:test") - cacheRef := path.Join(registry, "cache:test") - cacheOutDir, cacheOutDir2 := t.TempDir(), t.TempDir() - - res, err := c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "local", - Attrs: map[string]string{ - "dest": cacheOutDir, - }, - }, - { - Type: "local", - Attrs: map[string]string{ - "dest": cacheOutDir2, - }, - }, - { - Type: "registry", - Attrs: map[string]string{ - "ref": cacheRef, - }, - }, - { - Type: "inline", - }, - }, - }, nil) - require.NoError(t, err) - - ensureFile(t, filepath.Join(cacheOutDir, ocispecs.ImageIndexFile)) - ensureFile(t, filepath.Join(cacheOutDir2, ocispecs.ImageIndexFile)) - - dgst := res.ExporterResponse[exptypes.ExporterImageDigestKey] - - uniqueFile, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique") - require.NoError(t, err) - - res, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target2, - "push": "true", - }, - }, - }, - CacheExports: []CacheOptionsEntry{ - { - Type: "inline", - }, - }, - }, nil) - require.NoError(t, err) - - dgst2 := res.ExporterResponse[exptypes.ExporterImageDigestKey] - require.Equal(t, dgst, dgst2) - - destDir := t.TempDir() - ensurePruneAll(t, c, sb) - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterLocal, - OutputDir: destDir, - }, - }, - CacheImports: []CacheOptionsEntry{ - { - Type: "registry", - Attrs: map[string]string{ - "ref": cacheRef, - }, - }, - }, - }, nil) - require.NoError(t, err) - - ensureFileContents(t, filepath.Join(destDir, "const"), integration.UnixOrWindows("foobar", "foobar \r\n")) - ensureFileContents(t, filepath.Join(destDir, "unique"), string(uniqueFile)) -} - -// testMountStubsDirectory verifies that BuildKit cleans up stub directories created for tmpfs -// mounts after a Run step completes. Empty stubs should be removed, stubs with user content -// should be kept, and pre-existing directories used as mount targets should be preserved. -func testMountStubsDirectory(t *testing.T, sb integration.Sandbox) { - // Skipped on Windows because the test relies on tmpfs mounts, multiple simultaneous AddMount - // calls, and llb.Scratch() — none of which are supported on Windows containers. The stub - // cleanup behavior is also Linux-specific (tied to overlayfs mount point handling). - integration.SkipOnPlatform(t, "windows", "requires tmpfs, multiple mounts, and Scratch — all unsupported on Windows") - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - st := llb.Image("busybox:latest"). - File(llb.Mkdir("/test", 0700)). - File(llb.Mkdir("/test/qux/", 0700)). - Run( - llb.Args([]string{"touch", "/test/baz/keep"}), - // check stubs directory is removed - llb.AddMount("/test/foo", llb.Scratch(), llb.Tmpfs()), - // check that stubs directory are recursively removed - llb.AddMount("/test/bar/x/y", llb.Scratch(), llb.Tmpfs()), - // check that only empty stubs directories are removed - llb.AddMount("/test/baz/x", llb.Scratch(), llb.Tmpfs()), - // check that previously existing directory are not removed - llb.AddMount("/test/qux", llb.Scratch(), llb.Tmpfs()), - ).Root() - st = llb.Scratch().File(llb.Copy(st, "/test", "/", &llb.CopyInfo{CopyDirContentsOnly: true})) - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - tmpDir := t.TempDir() - tarFile := filepath.Join(tmpDir, "out.tar") - tarFileW, err := os.Create(tarFile) - require.NoError(t, err) - defer tarFileW.Close() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterTar, - Output: fixedWriteCloser(tarFileW), - }, - }, - }, nil) - require.NoError(t, err) - tarFileW.Close() - - dt, err := os.ReadFile(tarFile) - require.NoError(t, err) - - m, err := testutil.ReadTarToMap(dt, false) - require.NoError(t, err) - - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - - require.ElementsMatch(t, []string{ - "baz/", - "baz/keep", - "qux/", - }, keys) -} - -// testMountStubsTimestamp verifies that timestamps set on directories used as mount points -// (and their parents) are preserved after the mount is removed. -// https://github.com/moby/buildkit/issues/3148 -func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) { - // Skipped on Windows because the test relies on tmpfs mounts, multiple AddMount calls, - // Scratch, and Linux-specific commands — all unsupported on Windows containers. - integration.SkipOnPlatform(t, "windows", "requires tmpfs, multiple mounts, and Scratch — all unsupported on Windows") - c, err := New(sb.Context(), sb.Address()) - require.NoError(t, err) - defer c.Close() - - const sourceDateEpoch = int64(1234567890) // Fri Feb 13 11:31:30 PM UTC 2009 - st := llb.Image("busybox:latest").Run( - llb.Args([]string{ - "/bin/touch", fmt.Sprintf("--date=@%d", sourceDateEpoch), - "/bin", - "/etc", - "/var", - "/var/foo", - "/tmp", - "/tmp/foo2", - "/tmp/foo2/bar", - }), - llb.AddMount("/var/foo", llb.Scratch(), llb.Tmpfs()), - llb.AddMount("/tmp/foo2/bar", llb.Scratch(), llb.Tmpfs()), - ) - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - tmpDir := t.TempDir() - tarFile := filepath.Join(tmpDir, "out.tar") - tarFileW, err := os.Create(tarFile) - require.NoError(t, err) - defer tarFileW.Close() - - _, err = c.Solve(sb.Context(), def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterTar, - Output: fixedWriteCloser(tarFileW), - }, - }, - }, nil) - require.NoError(t, err) - tarFileW.Close() - - tarFileR, err := os.Open(tarFile) - require.NoError(t, err) - defer tarFileR.Close() - tarR := tar.NewReader(tarFileR) - touched := map[string]*tar.Header{ - "bin/": nil, // Regular dir - "etc/": nil, // Parent of file mounts (etc/{resolv.conf, hosts}) - "var/": nil, // Parent of dir mount (var/foo/) - "tmp/": nil, // Grandparent of dir mount (tmp/foo2/bar/) - // No support for reproducing the timestamps of mount point directories such as var/foo/ and tmp/foo2/bar/, - // because the touched timestamp value is lost when the mount is unmounted. - } - for { - hd, err := tarR.Next() - if errors.Is(err, io.EOF) { - break - } - require.NoError(t, err) - if x, ok := touched[hd.Name]; ok && x == nil { - touched[hd.Name] = hd - } - } - for name, hd := range touched { - t.Logf("Verifying %q (%+v)", name, hd) - require.NotNil(t, hd, name) - require.Equal(t, sourceDateEpoch, hd.ModTime.Unix(), name) - } -} - -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 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 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") -} - -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) -} - -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 -} - -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) -} - -func makeSSHAgentSock(t *testing.T, agent agent.Agent) (p string, err error) { - tmpDir := integration.Tmpdir(t) - sockPath := filepath.Join(tmpDir.Name, "ssh_auth_sock") - - listener := net.ListenConfig{} - l, err := listener.Listen(context.TODO(), "unix", sockPath) - if err != nil { - return "", err - } - t.Cleanup(func() { - require.NoError(t, l.Close()) - }) - - s := &server{l: l} - go s.run(agent) - - return sockPath, nil -} - -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 -} - -type server struct { - l net.Listener -} - -func (s *server) run(a agent.Agent) error { - for { - c, err := s.l.Accept() - if err != nil { - return err - } - - go agent.ServeAgent(a, c) - } -} - -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{} -) - -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{} -) - -func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc { - return func(map[string]string) (io.WriteCloser, error) { - return wc, nil - } -} - -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{pb.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) - }) - }) -} - -func testLLBMountPerformance(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") - srcPath := integration.UnixOrWindows("/bin", "/Windows") - - mntInput := llb.Image(imgName) - st := llb.Image(imgName) - var mnts []llb.State - // Reduce iterations on Windows due to significantly slower container operations - numIterations := integration.UnixOrWindows(20, 5) - for range numIterations { - execSt := st.Run( - llb.Args(integration.UnixOrWindows( - []string{"true"}, - []string{"cmd", "/C", "exit 0"}, - )), - ) - mnts = append(mnts, mntInput) - for j := range mnts { - mnts[j] = execSt.AddMount(fmt.Sprintf("/tmp/bin%d", j), mnts[j], llb.SourcePath(srcPath)) - } - st = execSt.Root() - } - - def, err := st.Marshal(sb.Context()) - require.NoError(t, err) - - // Windows images take longer time, especially on CI systems - // With reduced iterations (5 vs 20), use generous timeout - timeout := integration.UnixOrWindows(time.Minute, 10*time.Minute) - timeoutCtx, cancel := context.WithTimeoutCause(sb.Context(), timeout, nil) - defer cancel() - _, err = c.Solve(timeoutCtx, def, SolveOpt{}, nil) - require.NoError(t, err) -} - -func testLayerLimitOnMounts(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() - - base := llb.Image("busybox:latest") - - const numLayers = 110 - - for range numLayers { - base = base.Run(llb.Shlex("sh -c 'echo hello >> /hello'")).Root() - } - - def, err := base.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(ctx, def, SolveOpt{}, nil) - require.NoError(t, err) - - ls := llb.Image("busybox:latest"). - Run(llb.Shlexf("ls -l /base/hello")) - ls.AddMount("/base", base, llb.Readonly) - - def, err = ls.Marshal(sb.Context()) - require.NoError(t, err) - - _, err = c.Solve(ctx, def, SolveOpt{}, nil) - 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 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 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 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) -} - -func testImageResolveConfigDefaultLocalFallback(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter) - - cdAddress := sb.ContainerdAddress() - if cdAddress == "" { - t.Skip("test requires containerd worker") - } - - ctx := sb.Context() - c, err := New(ctx, sb.Address()) - require.NoError(t, err) - defer c.Close() - - cdClient, err := newContainerd(cdAddress) - require.NoError(t, err) - defer cdClient.Close() - - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - target := registry + "/buildkit/default-local-fallback:" + identity.NewID() - st := integration.UnixOrWindows( - llb.Scratch(), - llb.Image("nanoserver:latest"), - ) - def, err := st.File(llb.Mkfile("/fallback", 0600, []byte("local"))).Marshal(ctx) - require.NoError(t, err) - - _, err = c.Solve(ctx, def, SolveOpt{ - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "store": "true", - }, - }, - }, - }, nil) - require.NoError(t, err) - - imageService := cdClient.ImageService() - imageStoreCtx := namespaces.WithNamespace(ctx, "buildkit") - img, err := imageService.Get(imageStoreCtx, target) - require.NoError(t, err) - - _, err = c.Build(ctx, SolveOpt{}, "buildkit_test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - ref, dgst, config, err := c.ResolveImageConfig(ctx, target, sourceresolver.Opt{ - ImageOpt: &sourceresolver.ResolveImageOpt{ - ResolveMode: pb.AttrImageResolveModeDefault, - }, - }) - if err != nil { - return nil, err - } - require.Equal(t, target, ref) - require.Equal(t, img.Target.Digest, dgst) - require.NotEmpty(t, config) - - var ociimg ocispecs.Image - require.NoError(t, json.Unmarshal(config, &ociimg)) - require.NotEmpty(t, ociimg.OS) - require.NotEmpty(t, ociimg.Architecture) - - return gateway.NewResult(), nil - }, nil) - require.NoError(t, err) - - checkAllReleasable(t, c, sb, true) -} - -func testImageResolveAttestationChainRequiresNetwork(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureMultiPlatform, workers.FeatureProvenance) - // this test temporarily requires direct registry access as the integration test - // mirroring system does not support mirroring attestation chains yet. - // Support is coming in future buildx release. - ctx := sb.Context() - c, err := New(ctx, sb.Address()) - require.NoError(t, err) - defer c.Close() - - amd64, err := platforms.Parse("linux/amd64") - require.NoError(t, err) - - _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - const rootDigest = "sha256:4e91099af134f4b1f509fecbd1a55981dfff18d8029d6782e28413210ef468b7" - const imageDigest = "sha256:d2f0b39234a66c3d58f24200d3fda9e4e3d2263c09f9b7286a826ab639713047" - const attestationDigest = "sha256:fb4c46b14f52d1bf790f593921c52dddc698fc6780792fb8469fc60efc0e609b" - const sigDigest = "sha256:bd0a6b088440ba9838e8eec79e736128fe52afce934578eae30ca8675f6d3142" - - id := "registry-1-stage.docker.io/docker/github-builder-test@" + rootDigest - md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ - Identifier: "docker-image://" + id, - }, sourceresolver.Opt{ - ImageOpt: &sourceresolver.ResolveImageOpt{ - NoConfig: true, - AttestationChain: true, - Platform: &amd64, - }, - }) - if err != nil { - return nil, err - } - require.Equal(t, rootDigest, md.Image.Digest.String()) - require.Nil(t, md.Image.Config) - require.NotNil(t, md.Image) - require.NotNil(t, md.Image.AttestationChain) - ac := md.Image.AttestationChain - require.Equal(t, rootDigest, ac.Root.String()) - require.Equal(t, imageDigest, ac.ImageManifest.String()) - require.Equal(t, attestationDigest, ac.AttestationManifest.String()) - require.Len(t, ac.SignatureManifests, 1) - require.Equal(t, sigDigest, ac.SignatureManifests[0].String()) - - desc := ac.Blobs[ac.Root] - require.Equal(t, rootDigest, desc.Descriptor.Digest.String()) - require.Len(t, desc.Data, int(desc.Descriptor.Size)) - require.Equal(t, ocispecs.MediaTypeImageIndex, desc.Descriptor.MediaType) - chk := digest.FromBytes(desc.Data) - require.Equal(t, rootDigest, chk.String()) - - desc = ac.Blobs[ac.ImageManifest] - // image manifest is expected to be missing as content is not needed to verify attestation chain - _, ok := ac.Blobs[digest.Digest(imageDigest)] - require.False(t, ok) - - desc = ac.Blobs[ac.AttestationManifest] - require.Equal(t, attestationDigest, desc.Descriptor.Digest.String()) - require.Equal(t, ocispecs.MediaTypeImageManifest, desc.Descriptor.MediaType) - require.Len(t, desc.Data, int(desc.Descriptor.Size)) - chk = digest.FromBytes(desc.Data) - require.Equal(t, attestationDigest, chk.String()) - - desc = ac.Blobs[ac.SignatureManifests[0]] - require.Equal(t, sigDigest, desc.Descriptor.Digest.String()) - require.Equal(t, ocispecs.MediaTypeImageManifest, desc.Descriptor.MediaType) - require.Len(t, desc.Data, int(desc.Descriptor.Size)) - chk = digest.FromBytes(desc.Data) - require.Equal(t, sigDigest, chk.String()) - - var sigMfst ocispecs.Manifest - err = json.Unmarshal(ac.Blobs[ac.SignatureManifests[0]].Data, &sigMfst) - require.NoError(t, err) - require.Equal(t, 1, len(sigMfst.Layers)) - sigLayer := sigMfst.Layers[0] - sigDesc := ac.Blobs[digest.Digest(sigLayer.Digest)] - require.Equal(t, sigLayer.Digest, sigDesc.Descriptor.Digest) - require.Len(t, sigDesc.Data, int(sigDesc.Descriptor.Size)) - require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", sigDesc.Descriptor.MediaType) - chk = digest.FromBytes(sigDesc.Data) - require.Equal(t, sigLayer.Digest, chk) - - require.Len(t, md.Image.AttestationChain.Blobs, 4) - return nil, nil - }, nil) - require.NoError(t, err) -} - -func testImageResolveProvenanceAttestation(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) - requiresLinux(t) - - ctx := sb.Context() - c, err := New(ctx, sb.Address()) - require.NoError(t, err) - defer c.Close() - - target, platform := buildProvenanceImage(ctx, t, c, sb) - - _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ - Identifier: "docker-image://" + target, - }, sourceresolver.Opt{ - ImageOpt: &sourceresolver.ResolveImageOpt{ - NoConfig: true, - ResolveAttestations: []string{ - policyimage.SLSAProvenancePredicateType02, - policyimage.SLSAProvenancePredicateType1, - }, - Platform: &platform, - ResolveMode: pb.AttrImageResolveModeForcePull, - }, - }) - if err != nil { - return nil, err - } - require.NotNil(t, md.Image) - require.NotNil(t, md.Image.AttestationChain) - ac := md.Image.AttestationChain - require.NotEmpty(t, ac.AttestationManifest) - att := ac.Blobs[ac.AttestationManifest] - require.NotEmpty(t, att.Data) - - var manifest ocispecs.Manifest - require.NoError(t, json.Unmarshal(att.Data, &manifest)) - require.NotEmpty(t, manifest.Layers) - var ( - stmtBytes []byte - foundLayer ocispecs.Descriptor - ) - for _, layer := range manifest.Layers { - if !isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) { - continue - } - blob, ok := ac.Blobs[layer.Digest] - if !ok { - continue - } - stmtBytes = blob.Data - foundLayer = layer - break - } - require.NotEmpty(t, stmtBytes) - require.Contains(t, []string{ - policyimage.SLSAProvenancePredicateType02, - policyimage.SLSAProvenancePredicateType1, - }, foundLayer.Annotations["in-toto.io/predicate-type"]) - - var stmt intoto.Statement - require.NoError(t, json.Unmarshal(stmtBytes, &stmt)) - require.Equal(t, intoto.StatementInTotoV1, stmt.Type) - require.Contains(t, []string{ - policyimage.SLSAProvenancePredicateType02, - policyimage.SLSAProvenancePredicateType1, - }, stmt.PredicateType) - require.Equal(t, stmt.Subject[0].Digest["sha256"], ac.ImageManifest.Hex()) - return nil, nil - }, nil) - require.NoError(t, err) -} - -func testImageResolveAttestationChainLocal(t *testing.T, sb integration.Sandbox) { - workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance) - requiresLinux(t) - - ctx := sb.Context() - c, err := New(ctx, sb.Address()) - require.NoError(t, err) - defer c.Close() - - target, platform := buildProvenanceImage(ctx, t, c, sb) - - _, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{ - Identifier: "docker-image://" + target, - }, sourceresolver.Opt{ - ImageOpt: &sourceresolver.ResolveImageOpt{ - NoConfig: true, - AttestationChain: true, - Platform: &platform, - ResolveMode: pb.AttrImageResolveModeForcePull, - }, - }) - if err != nil { - return nil, err - } - require.NotNil(t, md.Image) - require.NotNil(t, md.Image.AttestationChain) - ac := md.Image.AttestationChain - require.NotEmpty(t, ac.AttestationManifest) - att := ac.Blobs[ac.AttestationManifest] - require.NotEmpty(t, att.Data) - - var manifest ocispecs.Manifest - require.NoError(t, json.Unmarshal(att.Data, &manifest)) - require.NotEmpty(t, manifest.Layers) - found := false - for _, layer := range manifest.Layers { - if isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) { - found = true - break - } - } - require.True(t, found) - return nil, nil - }, nil) - require.NoError(t, err) -} - -func buildProvenanceImage(ctx context.Context, t *testing.T, c *Client, sb integration.Sandbox) (string, ocispecs.Platform) { - t.Helper() - - registry, err := sb.NewRegistry() - if errors.Is(err, integration.ErrRequirements) { - t.Skip(err.Error()) - } - require.NoError(t, err) - - platform := platforms.Normalize(platforms.DefaultSpec()) - platformKey := platforms.Format(platform) - target := registry + "/buildkit/testprovenance:latest" - - frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { - res := gateway.NewResult() - - st := llb.Scratch().File( - llb.Mkfile("/greeting", 0600, []byte("hello provenance")), - ) - 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(platformKey, ref) - - img := ocispecs.Image{ - Platform: platform, - } - config, err := json.Marshal(img) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal image config") - } - res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, platformKey), config) - - expPlatforms := &exptypes.Platforms{ - Platforms: []exptypes.Platform{{ID: platformKey, Platform: platform}}, - } - dt, err := json.Marshal(expPlatforms) - if err != nil { - return nil, err - } - res.AddMeta(exptypes.ExporterPlatformsKey, dt) - - return res, nil - } - - _, err = c.Build(ctx, SolveOpt{ - FrontendAttrs: map[string]string{ - "attest:provenance": "mode=max,version=v1", - }, - Exports: []ExportEntry{ - { - Type: ExporterImage, - Attrs: map[string]string{ - "name": target, - "push": "true", - }, - }, - }, - }, "", frontend, nil) - require.NoError(t, err) - - return target, platform -} - -// isSLSAPredicateType reports whether the predicate type represents SLSA provenance. -func isSLSAPredicateType(v string) bool { - switch v { - case policyimage.SLSAProvenancePredicateType02, policyimage.SLSAProvenancePredicateType1: - return true - default: - return false - } -} - -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 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) -} - -// 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 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 -} - -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() -} - -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 - } - } -} - -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 -} diff --git a/client/client_utils_test.go b/client/client_utils_test.go new file mode 100644 index 000000000..86fd562ba --- /dev/null +++ b/client/client_utils_test.go @@ -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") +} diff --git a/client/client_warnings_test.go b/client/client_warnings_test.go new file mode 100644 index 000000000..1998e3ba1 --- /dev/null +++ b/client/client_warnings_test.go @@ -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() +} diff --git a/client/gateway_container_exec_test.go b/client/gateway_container_exec_test.go new file mode 100644 index 000000000..eb8348551 --- /dev/null +++ b/client/gateway_container_exec_test.go @@ -0,0 +1,1272 @@ +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: "% ", + } +} diff --git a/client/gateway_container_mount_test.go b/client/gateway_container_mount_test.go new file mode 100644 index 000000000..080b8a507 --- /dev/null +++ b/client/gateway_container_mount_test.go @@ -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) +} diff --git a/client/gateway_container_network_test.go b/client/gateway_container_network_test.go new file mode 100644 index 000000000..6e302f5d4 --- /dev/null +++ b/client/gateway_container_network_test.go @@ -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) +} diff --git a/client/gateway_container_security_test.go b/client/gateway_container_security_test.go new file mode 100644 index 000000000..068c100f2 --- /dev/null +++ b/client/gateway_container_security_test.go @@ -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) +} diff --git a/client/gateway_solve_test.go b/client/gateway_solve_test.go new file mode 100644 index 000000000..c2e592a32 --- /dev/null +++ b/client/gateway_solve_test.go @@ -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)) +} diff --git a/client/policy_test.go b/client/policy_test.go index 3e870b0f2..da15a70b9 100644 --- a/client/policy_test.go +++ b/client/policy_test.go @@ -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) + }) + }) +}