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:
Tonis Tiigi
2023-08-30 17:38:00 -07:00
parent 05eb728753
commit 1bbf73efa9
3 changed files with 196 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -170,6 +170,7 @@ var allTests = integration.TestFuncs(
testMultiPlatformWarnings,
testNilContextInSolveGateway,
testCopyUnicodePath,
testFrontendDeduplicateSources,
)
// Tests that depend on the `security.*` entitlements