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+ }
0 commit comments