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 Apr 10, 2024
1 parent a9575a8 commit 4e034e5
Show file tree
Hide file tree
Showing 19 changed files with 845 additions and 152 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jobs:
driver-opt: qemu.install=true
- driver: remote
endpoint: tcp://localhost:1234
- driver: docker-container
metadata-provenance: max
exclude:
- driver: docker
multi-node: mnode-true
Expand Down Expand Up @@ -129,6 +131,9 @@ jobs:
else
echo "MULTI_NODE=0" >> $GITHUB_ENV
fi
if [ -n "${{ matrix.metadata-provenance }}" ]; then
echo "BUILDX_METADATA_PROVENANCE=${{ matrix.metadata-provenance }}" >> $GITHUB_ENV
fi
-
name: Install k3s
if: matrix.driver == 'kubernetes'
Expand Down
6 changes: 6 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Options struct {
Session []session.Attachable
Linked bool // Linked marks this target as exclusively linked (not requested by the user).
PrintFunc *PrintFunc
MetadataFile string
SourcePolicy *spb.Policy
GroupRef string
}
Expand Down Expand Up @@ -488,6 +489,11 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s
rr.ExporterResponse[k] = string(v)
}
rr.ExporterResponse["buildx.build.ref"] = buildRef
if len(opt.MetadataFile) > 0 && 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
207 changes: 207 additions & 0 deletions build/provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package build

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strings"
"sync"
"time"

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

type ProvenancePredicate struct {
provenancetypes.ProvenancePredicate
Builder ProvenanceBuilder `json:"builder,omitempty"`
}

type ProvenanceBuilder struct {
ID string `json:"id,omitempty"`
}

func setRecordProvenance(ctx context.Context, c *client.Client, sr *client.SolveResponse, ref string, pw progress.Writer) error {
mode := confutil.MetadataProvenance()
if mode == confutil.MetadataProvenanceModeOff {
return nil
}
pw = progress.ResetTime(pw)
return progress.Wrap("resolving provenance for metadata file", pw.Write, func(l progress.SubLogger) error {
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 5 * time.Second
return backoff.Retry(func() error {
res, err := fetchProvenance(ctx, c, ref, mode, l)
if err != nil {
return err
}
for k, v := range res {
sr.ExporterResponse[k] = v
}
return nil
}, bo)
})
}

func fetchProvenance(ctx context.Context, c *client.Client, ref string, mode confutil.MetadataProvenanceMode, l progress.SubLogger) (out map[string]string, err error) {
cl, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
Ref: ref,
EarlyExit: true,
})
if err != nil {
return nil, err
}

var mu sync.Mutex
eg, ctx := errgroup.WithContext(ctx)
for {
ev, err := cl.Recv()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, err
}
if ev.Record == nil {
continue
}
if ev.Record.Result != nil {
provenanceDgst := provenanceDigest(ev.Record.Result)
if provenanceDgst == nil {
continue
}
eg.Go(func() error {
dt, err := getBlob(ctx, c, *provenanceDgst, "", l)
if err != nil {
return errors.Wrapf(err, "failed to load provenance from build record")
}
prv, err := encodeProvenance(dt, mode)
if err != nil {
return err
}
mu.Lock()
if out == nil {
out = make(map[string]string)
}
out["buildx.build.provenance"] = prv
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 {
dt, err := getBlob(ctx, c, *provenanceDgst, platform, l)
if err != nil {
return errors.Wrapf(err, "failed to load provenance from build record")
}
prv, err := encodeProvenance(dt, mode)
if err != nil {
return err
}
mu.Lock()
if out == nil {
out = make(map[string]string)
}
out["buildx.build.provenance/"+platform] = prv
mu.Unlock()
return nil
})
}
}
}
return out, 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 encodeProvenance(dt []byte, mode confutil.MetadataProvenanceMode) (string, error) {
if mode == confutil.MetadataProvenanceModeMax {
return base64.StdEncoding.EncodeToString(dt), nil
}

var prv ProvenancePredicate
if err := json.Unmarshal(dt, &prv); err != nil {
return "", errors.Wrapf(err, "failed to unmarshal provenance")
}

// remove buildConfig and metadata for minimal provenance
prv.BuildConfig = nil
prv.Metadata = nil

dtmin, err := json.Marshal(prv)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal minimal provenance")
}

return base64.StdEncoding.EncodeToString(dtmin), nil
}

func getBlob(ctx context.Context, c *client.Client, dgst digest.Digest, platform string, l progress.SubLogger) (dt []byte, err error) {
id := "fetching " + dgst.String()
if platform != "" {
id = fmt.Sprintf("[%s] fetching %s", platform, dgst.String())
}
st := &client.VertexStatus{
ID: id,
}

defer func() {
now := time.Now()
st.Completed = &now
if err == nil {
st.Total = st.Current
}
l.SetStatus(st)
}()

now := time.Now()
st.Started = &now
l.SetStatus(st)

store := proxy.NewContentStore(c.ContentClient())
ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{
Digest: dgst,
})
if err != nil {
return nil, err
}
defer ra.Close()

for {
buf := make([]byte, 1024)
n, err := ra.ReadAt(buf, st.Current)
if err != nil && err != io.EOF {
return nil, err
}
dt = append(dt, buf[:n]...)
st.Current += int64(n)
l.SetStatus(st)
if err == io.EOF {
break
}
}

return dt, nil
}
2 changes: 1 addition & 1 deletion commands/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,12 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
return nil
}

// local state group
groupRef := identity.NewID()
var refs []string
for k, b := range bo {
b.Ref = identity.NewID()
b.GroupRef = groupRef
b.MetadataFile = in.metadataFile
refs = append(refs, b.Ref)
bo[k] = b
}
Expand Down
5 changes: 3 additions & 2 deletions commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func (o *buildOptions) toControllerOptions() (*controllerapi.BuildOptions, error
Pull: o.pull,
ExportPush: o.exportPush,
ExportLoad: o.exportLoad,
MetadataFile: o.metadataFile,
}

// TODO: extract env var parsing to a method easily usable by library consumers
Expand Down Expand Up @@ -582,7 +583,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debug.D
flags.StringVarP(&options.dockerfileName, "file", "f", "", `Name of the Dockerfile (default: "PATH/Dockerfile")`)
flags.SetAnnotation("file", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/image/build/#file"})

flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to a file")

flags.StringArrayVar(&options.labels, "label", []string{}, "Set metadata for an image")

Expand Down Expand Up @@ -697,7 +698,7 @@ func commonBuildFlags(options *commonFlags, flags *pflag.FlagSet) {
options.noCache = flags.Bool("no-cache", false, "Do not use cache when building the image")
flags.StringVar(&options.progress, "progress", "auto", `Set type of progress output ("auto", "plain", "tty"). Use plain to show container output`)
options.pull = flags.Bool("pull", false, "Always attempt to pull all referenced images")
flags.StringVar(&options.metadataFile, "metadata-file", "", "Write build result metadata to the file")
flags.StringVar(&options.metadataFile, "metadata-file", "", "Write build result metadata to a file")
}

func checkWarnedFlags(f *pflag.Flag) {
Expand Down
1 change: 1 addition & 0 deletions controller/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.Build
Target: in.Target,
Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits),
GroupRef: in.GroupRef,
MetadataFile: in.MetadataFile,
}

platforms, err := platformutil.Parse(in.Platforms)
Expand Down
Loading

0 comments on commit 4e034e5

Please sign in to comment.