Skip to content

Commit 980ea2d

Browse files
authored
Merge pull request moby#4190 from tonistiigi/duplicate-sourcemap
llb: avoid duplicate instances of sourcemaps in provenance
2 parents e2dfabd + 1bbf73e commit 980ea2d

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-2
lines changed

client/llb/sourcemap.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package llb
22

33
import (
4+
"bytes"
45
"context"
56

67
"github.com/moby/buildkit/solver/pb"
@@ -47,6 +48,33 @@ func (s *SourceMap) Location(r []*pb.Range) ConstraintsOpt {
4748
})
4849
}
4950

51+
func equalSourceMap(sm1, sm2 *SourceMap) (out bool) {
52+
if sm1 == nil || sm2 == nil {
53+
return false
54+
}
55+
if sm1.Filename != sm2.Filename {
56+
return false
57+
}
58+
if sm1.Language != sm2.Language {
59+
return false
60+
}
61+
if len(sm1.Data) != len(sm2.Data) {
62+
return false
63+
}
64+
if !bytes.Equal(sm1.Data, sm2.Data) {
65+
return false
66+
}
67+
if sm1.Definition != nil && sm2.Definition != nil {
68+
if len(sm1.Definition.Def) != len(sm2.Definition.Def) && len(sm1.Definition.Def) != 0 {
69+
return false
70+
}
71+
if !bytes.Equal(sm1.Definition.Def[len(sm1.Definition.Def)-1], sm2.Definition.Def[len(sm2.Definition.Def)-1]) {
72+
return false
73+
}
74+
}
75+
return true
76+
}
77+
5078
type SourceLocation struct {
5179
SourceMap *SourceMap
5280
Ranges []*pb.Range
@@ -69,8 +97,18 @@ func (smc *sourceMapCollector) Add(dgst digest.Digest, ls []*SourceLocation) {
6997
for _, l := range ls {
7098
idx, ok := smc.index[l.SourceMap]
7199
if !ok {
72-
idx = len(smc.maps)
73-
smc.maps = append(smc.maps, l.SourceMap)
100+
idx = -1
101+
// slow equality check
102+
for i, m := range smc.maps {
103+
if equalSourceMap(m, l.SourceMap) {
104+
idx = i
105+
break
106+
}
107+
}
108+
if idx == -1 {
109+
idx = len(smc.maps)
110+
smc.maps = append(smc.maps, l.SourceMap)
111+
}
74112
}
75113
smc.index[l.SourceMap] = idx
76114
}

frontend/dockerfile/dockerfile_provenance_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"net/http/httptest"
910
"net/url"
@@ -16,16 +17,20 @@ import (
1617

1718
"github.com/containerd/containerd/content"
1819
"github.com/containerd/containerd/content/local"
20+
"github.com/containerd/containerd/content/proxy"
1921
"github.com/containerd/containerd/platforms"
2022
"github.com/containerd/continuity/fs/fstest"
2123
intoto "github.com/in-toto/in-toto-golang/in_toto"
2224
provenanceCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
25+
controlapi "github.com/moby/buildkit/api/services/control"
2326
"github.com/moby/buildkit/client"
2427
"github.com/moby/buildkit/client/llb"
2528
"github.com/moby/buildkit/exporter/containerimage/exptypes"
2629
"github.com/moby/buildkit/frontend/dockerui"
2730
gateway "github.com/moby/buildkit/frontend/gateway/client"
31+
"github.com/moby/buildkit/identity"
2832
"github.com/moby/buildkit/solver/llbsolver/provenance"
33+
"github.com/moby/buildkit/solver/pb"
2934
"github.com/moby/buildkit/util/contentutil"
3035
"github.com/moby/buildkit/util/testutil"
3136
"github.com/moby/buildkit/util/testutil/integration"
@@ -1113,3 +1118,153 @@ func testDockerIgnoreMissingProvenance(t *testing.T, sb integration.Sandbox) {
11131118
}, "", frontend, nil)
11141119
require.NoError(t, err)
11151120
}
1121+
1122+
func testFrontendDeduplicateSources(t *testing.T, sb integration.Sandbox) {
1123+
ctx := sb.Context()
1124+
1125+
c, err := client.New(ctx, sb.Address())
1126+
require.NoError(t, err)
1127+
defer c.Close()
1128+
1129+
dockerfile := []byte(`
1130+
FROM scratch as base
1131+
COPY foo foo2
1132+
1133+
FROM linked
1134+
COPY bar bar2
1135+
`)
1136+
1137+
dir := integration.Tmpdir(
1138+
t,
1139+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
1140+
fstest.CreateFile("foo", []byte("data"), 0600),
1141+
fstest.CreateFile("bar", []byte("data2"), 0600),
1142+
)
1143+
1144+
f := getFrontend(t, sb)
1145+
1146+
b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
1147+
res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{
1148+
FrontendOpt: map[string]string{
1149+
"target": "base",
1150+
},
1151+
})
1152+
if err != nil {
1153+
return nil, err
1154+
}
1155+
ref, err := res.SingleRef()
1156+
if err != nil {
1157+
return nil, err
1158+
}
1159+
st, err := ref.ToState()
1160+
if err != nil {
1161+
return nil, err
1162+
}
1163+
1164+
def, err := st.Marshal(ctx)
1165+
if err != nil {
1166+
return nil, err
1167+
}
1168+
1169+
dt, ok := res.Metadata["containerimage.config"]
1170+
if !ok {
1171+
return nil, errors.Errorf("no containerimage.config in metadata")
1172+
}
1173+
1174+
dt, err = json.Marshal(map[string][]byte{
1175+
"containerimage.config": dt,
1176+
})
1177+
if err != nil {
1178+
return nil, err
1179+
}
1180+
1181+
res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{
1182+
FrontendOpt: map[string]string{
1183+
"context:linked": "input:baseinput",
1184+
"input-metadata:linked": string(dt),
1185+
},
1186+
FrontendInputs: map[string]*pb.Definition{
1187+
"baseinput": def.ToPB(),
1188+
},
1189+
})
1190+
if err != nil {
1191+
return nil, err
1192+
}
1193+
return res, nil
1194+
}
1195+
1196+
product := "buildkit_test"
1197+
1198+
destDir := t.TempDir()
1199+
1200+
ref := identity.NewID()
1201+
1202+
_, err = c.Build(ctx, client.SolveOpt{
1203+
LocalDirs: map[string]string{
1204+
dockerui.DefaultLocalNameDockerfile: dir,
1205+
dockerui.DefaultLocalNameContext: dir,
1206+
},
1207+
Exports: []client.ExportEntry{
1208+
{
1209+
Type: client.ExporterLocal,
1210+
OutputDir: destDir,
1211+
},
1212+
},
1213+
Ref: ref,
1214+
}, product, b, nil)
1215+
require.NoError(t, err)
1216+
1217+
dt, err := os.ReadFile(filepath.Join(destDir, "foo2"))
1218+
require.NoError(t, err)
1219+
require.Equal(t, "data", string(dt))
1220+
1221+
dt, err = os.ReadFile(filepath.Join(destDir, "bar2"))
1222+
require.NoError(t, err)
1223+
require.Equal(t, "data2", string(dt))
1224+
1225+
history, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
1226+
Ref: ref,
1227+
EarlyExit: true,
1228+
})
1229+
require.NoError(t, err)
1230+
1231+
store := proxy.NewContentStore(c.ContentClient())
1232+
1233+
var provDt []byte
1234+
for {
1235+
ev, err := history.Recv()
1236+
if err != nil {
1237+
require.Equal(t, io.EOF, err)
1238+
break
1239+
}
1240+
require.Equal(t, ref, ev.Record.Ref)
1241+
1242+
for _, prov := range ev.Record.Result.Attestations {
1243+
if len(prov.Annotations) == 0 || prov.Annotations["in-toto.io/predicate-type"] != "https://slsa.dev/provenance/v0.2" {
1244+
t.Logf("skipping non-slsa provenance: %s", prov.MediaType)
1245+
continue
1246+
}
1247+
1248+
provDt, err = content.ReadBlob(ctx, store, ocispecs.Descriptor{
1249+
MediaType: prov.MediaType,
1250+
Digest: prov.Digest,
1251+
Size: prov.Size_,
1252+
})
1253+
require.NoError(t, err)
1254+
}
1255+
}
1256+
1257+
require.NotEqual(t, len(provDt), 0)
1258+
1259+
var pred provenance.ProvenancePredicate
1260+
require.NoError(t, json.Unmarshal(provDt, &pred))
1261+
1262+
sources := pred.Metadata.BuildKitMetadata.Source.Infos
1263+
1264+
require.Equal(t, 1, len(sources))
1265+
require.Equal(t, "Dockerfile", sources[0].Filename)
1266+
require.Equal(t, "Dockerfile", sources[0].Language)
1267+
1268+
require.Equal(t, dockerfile, sources[0].Data)
1269+
require.NotEqual(t, 0, len(sources[0].Definition))
1270+
}

frontend/dockerfile/dockerfile_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ var allTests = integration.TestFuncs(
170170
testMultiPlatformWarnings,
171171
testNilContextInSolveGateway,
172172
testCopyUnicodePath,
173+
testFrontendDeduplicateSources,
173174
)
174175

175176
// Tests that depend on the `security.*` entitlements

0 commit comments

Comments
 (0)