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