mirror of
https://github.com/moby/buildkit.git
synced 2026-08-08 08:40:45 +00:00
llb: avoid duplicate instances of sourcemaps in provenance
If build contains multiple subbuilds all of their sources are tracked in provenance attestations. When some subbuilds are coming from same source file (eg. same Dockerfile but different targets) currently the same file would appear in multiple times. This detects such duplicates and makes sure definitions from multiple subbuilds can map to same file. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ var allTests = integration.TestFuncs(
|
||||
testMultiPlatformWarnings,
|
||||
testNilContextInSolveGateway,
|
||||
testCopyUnicodePath,
|
||||
testFrontendDeduplicateSources,
|
||||
)
|
||||
|
||||
// Tests that depend on the `security.*` entitlements
|
||||
|
||||
Reference in New Issue
Block a user