Skip to content

Commit

Permalink
build: set record provenance in response
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed Feb 24, 2024
1 parent ae0a5e4 commit 4a00e0f
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 7 deletions.
5 changes: 5 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,11 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
rr.ExporterResponse[k] = string(v)
}
rr.ExporterResponse["buildx.build.ref"] = buildRef
if node.Driver.HistoryAPISupported(ctx) {
if err := setRecordProvenance(ctx, c, rr, so.Ref, pw); err != nil {
return err
}
}

node := dp.Node().Driver
if node.IsMobyDriver() {
Expand Down
104 changes: 104 additions & 0 deletions build/provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package build

import (
"bytes"
"context"
"encoding/base64"
"io"
"strings"
"sync"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/content/proxy"
"github.com/docker/buildx/util/progress"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

func setRecordProvenance(ctx context.Context, c *client.Client, sr *client.SolveResponse, ref string, pw progress.Writer) error {
var mu sync.Mutex
pw = progress.ResetTime(pw)
return progress.Wrap("fetching build record provenance", pw.Write, func(l progress.SubLogger) error {
cl, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
Ref: ref,
EarlyExit: true,
})
if err != nil {
return err
}

eg, ctx := errgroup.WithContext(ctx)
for {
ev, err := cl.Recv()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return err
}
if ev.Record == nil {
continue
}
if ev.Record.Result != nil {
provenanceDgst := provenanceDigest(ev.Record.Result)
if provenanceDgst == nil {
continue
}
eg.Go(func() error {
buf := &bytes.Buffer{}
if err := writeBlob(ctx, c, *provenanceDgst, buf); err != nil {
return errors.Wrapf(err, "failed to load provenance from build record")
}
mu.Lock()
sr.ExporterResponse["buildx.build.provenance"] = base64.StdEncoding.EncodeToString(buf.Bytes())
mu.Unlock()
return nil
})
} else if ev.Record.Results != nil {
for platform, res := range ev.Record.Results {
platform := platform
provenanceDgst := provenanceDigest(res)
if provenanceDgst == nil {
continue
}
eg.Go(func() error {
buf := &bytes.Buffer{}
if err := writeBlob(ctx, c, *provenanceDgst, buf); err != nil {
return errors.Wrapf(err, "failed to load provenance from build record")
}
mu.Lock()
sr.ExporterResponse["buildx.build.provenance/"+platform] = base64.StdEncoding.EncodeToString(buf.Bytes())
mu.Unlock()
return nil
})
}
}
}
return eg.Wait()
})
}

func provenanceDigest(res *controlapi.BuildResultInfo) *digest.Digest {
for _, a := range res.Attestations {
if a.MediaType == "application/vnd.in-toto+json" && strings.HasPrefix(a.Annotations["in-toto.io/predicate-type"], "https://slsa.dev/provenance/") {
return &a.Digest
}
}
return nil
}

func writeBlob(ctx context.Context, c *client.Client, dgst digest.Digest, w io.Writer) error {
store := proxy.NewContentStore(c.ContentClient())
ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{
Digest: dgst,
})
if err != nil {
return err
}
defer ra.Close()
_, err = io.Copy(w, content.NewReader(ra))
return err
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/google/uuid v1.5.0
github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992
github.com/hashicorp/hcl/v2 v2.19.1
github.com/in-toto/in-toto-golang v0.5.0
github.com/moby/buildkit v0.13.0-rc1.0.20240222164755-8e3fe35738c2 // master (v0.13.0-dev)
github.com/moby/sys/mountinfo v0.7.1
github.com/moby/sys/signal v0.7.0
Expand Down Expand Up @@ -105,7 +106,6 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
16 changes: 13 additions & 3 deletions tests/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/containerd/continuity/fs/fstest"
"github.com/docker/buildx/util/gitutil"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -35,7 +36,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakeEmpty,
testBakeShmSize,
testBakeUlimits,
testBakeRefs,
testBakeMetadata,
}

func testBakeLocal(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -589,7 +590,7 @@ target "default" {
require.Contains(t, string(dt), `1024`)
}

func testBakeRefs(t *testing.T, sb integration.Sandbox) {
func testBakeMetadata(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM scratch
COPY foo /foo
Expand Down Expand Up @@ -622,12 +623,21 @@ target "default" {

type mdT struct {
Default struct {
BuildRef string `json:"buildx.build.ref"`
BuildRef string `json:"buildx.build.ref"`
BuildProvenance map[string]interface{} `json:"buildx.build.provenance"`
} `json:"default"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)

require.NotEmpty(t, md.Default.BuildRef)
require.NotEmpty(t, md.Default.BuildProvenance)

dtprv, err := json.Marshal(md.Default.BuildProvenance)
require.NoError(t, err)

var prv slsa02.ProvenancePredicate
require.NoError(t, json.Unmarshal(dtprv, &prv))
require.Equal(t, "https://mobyproject.org/buildkit@v1", prv.BuildType)
}
16 changes: 13 additions & 3 deletions tests/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/containerd/containerd/platforms"
"github.com/containerd/continuity/fs/fstest"
"github.com/creack/pty"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/moby/buildkit/util/appdefaults"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/testutil"
Expand Down Expand Up @@ -53,7 +54,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildNetworkModeBridge,
testBuildShmSize,
testBuildUlimit,
testBuildRef,
testBuildMetadata,
}

func testBuild(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -515,7 +516,7 @@ COPY --from=build /ulimit /
require.Contains(t, string(dt), `1024`)
}

func testBuildRef(t *testing.T, sb integration.Sandbox) {
func testBuildMetadata(t *testing.T, sb integration.Sandbox) {
dir := createTestProject(t)
dirDest := t.TempDir()

Expand All @@ -533,13 +534,22 @@ func testBuildRef(t *testing.T, sb integration.Sandbox) {
require.NoError(t, err)

type mdT struct {
BuildRef string `json:"buildx.build.ref"`
BuildRef string `json:"buildx.build.ref"`
BuildProvenance map[string]interface{} `json:"buildx.build.provenance"`
}
var md mdT
err = json.Unmarshal(dt, &md)
require.NoError(t, err)

require.NotEmpty(t, md.BuildRef)
require.NotEmpty(t, md.BuildProvenance)

dtprv, err := json.Marshal(md.BuildProvenance)
require.NoError(t, err)

var prv slsa02.ProvenancePredicate
require.NoError(t, json.Unmarshal(dtprv, &prv))
require.Equal(t, "https://mobyproject.org/buildkit@v1", prv.BuildType)
}

func createTestProject(t *testing.T) string {
Expand Down

0 comments on commit 4a00e0f

Please sign in to comment.