diff --git a/client/llb/sourcemap.go b/client/llb/sourcemap.go index 721db3cebe9d..4e3be2b49936 100644 --- a/client/llb/sourcemap.go +++ b/client/llb/sourcemap.go @@ -1,6 +1,7 @@ package llb import ( + "bytes" "context" "github.com/moby/buildkit/solver/pb" @@ -47,6 +48,33 @@ func (s *SourceMap) Location(r []*pb.Range) ConstraintsOpt { }) } +func equalSourceMap(sm1, sm2 *SourceMap) (out bool) { + if sm1 == nil || sm2 == nil { + return false + } + if sm1.Filename != sm2.Filename { + return false + } + if sm1.Language != sm2.Language { + return false + } + if len(sm1.Data) != len(sm2.Data) { + return false + } + if !bytes.Equal(sm1.Data, sm2.Data) { + return false + } + if sm1.Definition != nil && sm2.Definition != nil { + if len(sm1.Definition.Def) != len(sm2.Definition.Def) && len(sm1.Definition.Def) != 0 { + return false + } + if !bytes.Equal(sm1.Definition.Def[len(sm1.Definition.Def)-1], sm2.Definition.Def[len(sm2.Definition.Def)-1]) { + return false + } + } + return true +} + type SourceLocation struct { SourceMap *SourceMap Ranges []*pb.Range @@ -69,8 +97,18 @@ func (smc *sourceMapCollector) Add(dgst digest.Digest, ls []*SourceLocation) { for _, l := range ls { idx, ok := smc.index[l.SourceMap] if !ok { - idx = len(smc.maps) - smc.maps = append(smc.maps, l.SourceMap) + idx = -1 + // slow equality check + for i, m := range smc.maps { + if equalSourceMap(m, l.SourceMap) { + idx = i + break + } + } + if idx == -1 { + idx = len(smc.maps) + smc.maps = append(smc.maps, l.SourceMap) + } } smc.index[l.SourceMap] = idx } diff --git a/frontend/dockerfile/dockerfile_provenance_test.go b/frontend/dockerfile/dockerfile_provenance_test.go index 333b4a2a76f2..fd4de154827b 100644 --- a/frontend/dockerfile/dockerfile_provenance_test.go +++ b/frontend/dockerfile/dockerfile_provenance_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "net/url" @@ -16,16 +17,20 @@ import ( "github.com/containerd/containerd/content" "github.com/containerd/containerd/content/local" + "github.com/containerd/containerd/content/proxy" "github.com/containerd/containerd/platforms" "github.com/containerd/continuity/fs/fstest" intoto "github.com/in-toto/in-toto-golang/in_toto" provenanceCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + controlapi "github.com/moby/buildkit/api/services/control" "github.com/moby/buildkit/client" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/exporter/containerimage/exptypes" "github.com/moby/buildkit/frontend/dockerui" gateway "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/identity" "github.com/moby/buildkit/solver/llbsolver/provenance" + "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" @@ -1113,3 +1118,153 @@ func testDockerIgnoreMissingProvenance(t *testing.T, sb integration.Sandbox) { }, "", frontend, nil) require.NoError(t, err) } + +func testFrontendDeduplicateSources(t *testing.T, sb integration.Sandbox) { + ctx := sb.Context() + + c, err := client.New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + dockerfile := []byte(` +FROM scratch as base +COPY foo foo2 + +FROM linked +COPY bar bar2 +`) + + dir := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte("data"), 0600), + fstest.CreateFile("bar", []byte("data2"), 0600), + ) + + f := getFrontend(t, sb) + + b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { + res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{ + FrontendOpt: map[string]string{ + "target": "base", + }, + }) + if err != nil { + return nil, err + } + ref, err := res.SingleRef() + if err != nil { + return nil, err + } + st, err := ref.ToState() + if err != nil { + return nil, err + } + + def, err := st.Marshal(ctx) + if err != nil { + return nil, err + } + + dt, ok := res.Metadata["containerimage.config"] + if !ok { + return nil, errors.Errorf("no containerimage.config in metadata") + } + + dt, err = json.Marshal(map[string][]byte{ + "containerimage.config": dt, + }) + if err != nil { + return nil, err + } + + res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{ + FrontendOpt: map[string]string{ + "context:linked": "input:baseinput", + "input-metadata:linked": string(dt), + }, + FrontendInputs: map[string]*pb.Definition{ + "baseinput": def.ToPB(), + }, + }) + if err != nil { + return nil, err + } + return res, nil + } + + product := "buildkit_test" + + destDir := t.TempDir() + + ref := identity.NewID() + + _, err = c.Build(ctx, client.SolveOpt{ + LocalDirs: map[string]string{ + dockerui.DefaultLocalNameDockerfile: dir, + dockerui.DefaultLocalNameContext: dir, + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir, + }, + }, + Ref: ref, + }, product, b, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "foo2")) + require.NoError(t, err) + require.Equal(t, "data", string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "bar2")) + require.NoError(t, err) + require.Equal(t, "data2", string(dt)) + + history, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{ + Ref: ref, + EarlyExit: true, + }) + require.NoError(t, err) + + store := proxy.NewContentStore(c.ContentClient()) + + var provDt []byte + for { + ev, err := history.Recv() + if err != nil { + require.Equal(t, io.EOF, err) + break + } + require.Equal(t, ref, ev.Record.Ref) + + for _, prov := range ev.Record.Result.Attestations { + if len(prov.Annotations) == 0 || prov.Annotations["in-toto.io/predicate-type"] != "https://slsa.dev/provenance/v0.2" { + t.Logf("skipping non-slsa provenance: %s", prov.MediaType) + continue + } + + provDt, err = content.ReadBlob(ctx, store, ocispecs.Descriptor{ + MediaType: prov.MediaType, + Digest: prov.Digest, + Size: prov.Size_, + }) + require.NoError(t, err) + } + } + + require.NotEqual(t, len(provDt), 0) + + var pred provenance.ProvenancePredicate + require.NoError(t, json.Unmarshal(provDt, &pred)) + + sources := pred.Metadata.BuildKitMetadata.Source.Infos + + require.Equal(t, 1, len(sources)) + require.Equal(t, "Dockerfile", sources[0].Filename) + require.Equal(t, "Dockerfile", sources[0].Language) + + require.Equal(t, dockerfile, sources[0].Data) + require.NotEqual(t, 0, len(sources[0].Definition)) +} diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 6ae527d01d7a..8abe4f321004 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -170,6 +170,7 @@ var allTests = integration.TestFuncs( testMultiPlatformWarnings, testNilContextInSolveGateway, testCopyUnicodePath, + testFrontendDeduplicateSources, ) // Tests that depend on the `security.*` entitlements