diff --git a/cache/remotecache/v1/chains.go b/cache/remotecache/v1/chains.go index 11ea24b865f8..d9f25a1416dd 100644 --- a/cache/remotecache/v1/chains.go +++ b/cache/remotecache/v1/chains.go @@ -2,7 +2,6 @@ package cacheimport import ( "context" - "strings" "sync" "time" @@ -22,10 +21,12 @@ type CacheChains struct { } func (c *CacheChains) Add(dgst digest.Digest) solver.CacheExporterRecord { - if strings.HasPrefix(dgst.String(), "random:") { - return &nopRecord{} + it := &item{ + c: c, + dgst: dgst, + backlinks: map[*item]struct{}{}, + skipContent: dgst.Algorithm() == "random", } - it := &item{c: c, dgst: dgst, backlinks: map[*item]struct{}{}} c.items = append(c.items, it) return it } @@ -120,6 +121,7 @@ type item struct { backlinksMu sync.Mutex backlinks map[*item]struct{} invalid bool + skipContent bool } type link struct { @@ -147,6 +149,9 @@ func (c *item) removeLink(src *item) bool { } func (c *item) AddResult(_ digest.Digest, _ int, createdAt time.Time, result *solver.Remote) { + if c.skipContent { + return + } c.resultTime = createdAt c.result = result } @@ -211,13 +216,4 @@ func (c *item) walkAllResults(fn func(i *item) error, visited map[*item]struct{} return nil } -type nopRecord struct { -} - -func (c *nopRecord) AddResult(_ digest.Digest, _ int, createdAt time.Time, result *solver.Remote) { -} - -func (c *nopRecord) LinkFrom(rec solver.CacheExporterRecord, index int, selector string) { -} - var _ solver.CacheExporterTarget = &CacheChains{} diff --git a/client/client_test.go b/client/client_test.go index 49b2fe7b6752..22c37778b5b3 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -56,6 +56,7 @@ import ( "github.com/moby/buildkit/util/attestation" "github.com/moby/buildkit/util/contentutil" "github.com/moby/buildkit/util/entitlements" + "github.com/moby/buildkit/util/staticfs" "github.com/moby/buildkit/util/testutil" containerdutil "github.com/moby/buildkit/util/testutil/containerd" "github.com/moby/buildkit/util/testutil/echoserver" @@ -68,6 +69,8 @@ import ( "github.com/pkg/errors" "github.com/spdx/tools-golang/spdx" "github.com/stretchr/testify/require" + "github.com/tonistiigi/fsutil" + "github.com/tonistiigi/fsutil/types" "golang.org/x/crypto/ssh/agent" "golang.org/x/sync/errgroup" "google.golang.org/grpc" @@ -208,6 +211,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){ testSnapshotWithMultipleBlobs, testExportLocalNoPlatformSplit, testExportLocalNoPlatformSplitOverwrite, + testExportStableLocalCache, } func TestIntegration(t *testing.T) { @@ -9472,6 +9476,111 @@ func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) { } } +func testExportStableLocalCache(t *testing.T, sb integration.Sandbox) { + requiresLinux(t) + 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/testrandomcache:latest" + + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + sessionID := make(chan string) + done := make(chan struct{}) + frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + sessionID <- c.BuildOpts().SessionID + <-done + return nil, nil + } + defer close(done) + mylocal := staticfs.NewFS() + mylocal.Add("foo", types.Stat{Mode: 0644}, []byte("bar")) + go c.Build(sb.Context(), SolveOpt{ + LocalMounts: map[string]fsutil.FS{ + "mylocal": mylocal, + }, + }, "", frontend, nil) + + busybox := llb.Image("busybox:latest") + st := llb.Local("mylocal", llb.SessionID(<-sessionID)) + + run := func(cmd string) { + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + } + + run(`sh -c "cat /wd/foo > const"`) + 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, + }, + }, + CacheExports: []CacheOptionsEntry{{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }}, + LocalMounts: map[string]fsutil.FS{ + "mylocal": mylocal, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "const")) + require.NoError(t, err) + require.Equal(t, string(dt), "bar") + 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: []CacheOptionsEntry{{ + Type: "registry", + Attrs: map[string]string{ + "ref": target, + }, + }}, + LocalMounts: map[string]fsutil.FS{ + "mylocal": mylocal, + }, + }, nil) + require.NoError(t, err) + + dt2, err := os.ReadFile(filepath.Join(destDir, "const")) + require.NoError(t, err) + require.Equal(t, string(dt2), "bar") + dt2, err = os.ReadFile(filepath.Join(destDir, "unique")) + require.NoError(t, err) + require.Equal(t, string(dt), string(dt2)) +} + func ensureFile(t *testing.T, path string) { st, err := os.Stat(path) require.NoError(t, err, "expected file at %s", path)