diff --git a/client/client_test.go b/client/client_test.go index 6795fb7736b5..2d56fde7e300 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -216,6 +216,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){ testExportLocalNoPlatformSplitOverwrite, testSolverOptLocalDirsStillWorks, testOCIIndexMediatype, + testLayerLimitOnMounts, } func TestIntegration(t *testing.T) { @@ -10242,6 +10243,40 @@ func testLLBMountPerformance(t *testing.T, sb integration.Sandbox) { 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 i := 0; i < numLayers; i++ { + 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 interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { diff --git a/executor/oci/spec.go b/executor/oci/spec.go index ec903bc6a610..89103e9de0b4 100644 --- a/executor/oci/spec.go +++ b/executor/oci/spec.go @@ -183,6 +183,16 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou } releasers = append(releasers, release) for _, mount := range mounts { + mount, release, err := compactLongOverlayMount(mount, m.Readonly) + if err != nil { + releaseAll() + return nil, nil, err + } + + if release != nil { + releasers = append(releasers, release) + } + mount, err = sm.subMount(mount, m.Selector) if err != nil { releaseAll() @@ -261,26 +271,8 @@ func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error) return mount.Mount{}, err } - var mntType string - opts := []string{} - if m.ReadOnly() { - opts = append(opts, "ro") - } - - if runtime.GOOS != "windows" { - // Windows uses a mechanism similar to bind mounts, but will err out if we request - // a mount type it does not understand. Leaving the mount type empty on Windows will - // yield the same result. - mntType = "bind" - opts = append(opts, "rbind") - } - s.m[h] = mountRef{ - mount: mount.Mount{ - Source: mp, - Type: mntType, - Options: opts, - }, + mount: bind(mp, m.ReadOnly()), unmount: lm.Unmount, subRefs: map[string]mountRef{}, } @@ -312,3 +304,45 @@ func (s *submounts) cleanup() { } wg.Wait() } + +func bind(p string, ro bool) mount.Mount { + m := mount.Mount{ + Source: p, + } + if runtime.GOOS != "windows" { + // Windows uses a mechanism similar to bind mounts, but will err out if we request + // a mount type it does not understand. Leaving the mount type empty on Windows will + // yield the same result. + m.Type = "bind" + m.Options = []string{"rbind"} + } + if ro { + m.Options = append(m.Options, "ro") + } + return m +} + +func compactLongOverlayMount(m mount.Mount, ro bool) (mount.Mount, func() error, error) { + if m.Type != "overlay" { + return m, nil, nil + } + + sz := 0 + for _, opt := range m.Options { + sz += len(opt) + 1 + } + + // can fit to single page, no need to compact + if sz < 4096-512 { + return m, nil, nil + } + + lm := snapshot.LocalMounterWithMounts([]mount.Mount{m}) + + mp, err := lm.Mount() + if err != nil { + return mount.Mount{}, nil, err + } + + return bind(mp, ro), lm.Unmount, nil +}