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 9, 2024
1 parent 0a3e5e5 commit eba0b61
Show file tree
Hide file tree
Showing 15 changed files with 695 additions and 30 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
5 changes: 5 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,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
198 changes: 198 additions & 0 deletions build/provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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"
)

func setRecordProvenance(ctx context.Context, c *client.Client, sr *client.SolveResponse, ref string, pw progress.Writer) error {
mode := confutil.MetadataProvenance()
if mode == confutil.MetadataProvenanceModeNone {
return nil
}
pw = progress.ResetTime(pw)
return progress.Wrap("resolve build record provenance", 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 provenancetypes.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
}
4 changes: 2 additions & 2 deletions commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,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 @@ -687,7 +687,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
98 changes: 84 additions & 14 deletions docs/reference/buildx_bake.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ Build from a file

### Options

| Name | Type | Default | Description |
|:---------------------------------|:--------------|:--------|:-----------------------------------------------------------------------------------------|
| [`--builder`](#builder) | `string` | | Override the configured builder instance |
| [`-f`](#file), [`--file`](#file) | `stringArray` | | Build definition file |
| `--load` | | | Shorthand for `--set=*.output=type=docker` |
| `--metadata-file` | `string` | | Write build result metadata to the file |
| [`--no-cache`](#no-cache) | | | Do not use cache when building the image |
| [`--print`](#print) | | | Print the options without building |
| [`--progress`](#progress) | `string` | `auto` | Set type of progress output (`auto`, `plain`, `tty`). Use plain to show container output |
| [`--provenance`](#provenance) | `string` | | Shorthand for `--set=*.attest=type=provenance` |
| [`--pull`](#pull) | | | Always attempt to pull all referenced images |
| `--push` | | | Shorthand for `--set=*.output=type=registry` |
| [`--sbom`](#sbom) | `string` | | Shorthand for `--set=*.attest=type=sbom` |
| [`--set`](#set) | `stringArray` | | Override target value (e.g., `targetpattern.key=value`) |
| Name | Type | Default | Description |
|:------------------------------------|:--------------|:--------|:-----------------------------------------------------------------------------------------|
| [`--builder`](#builder) | `string` | | Override the configured builder instance |
| [`-f`](#file), [`--file`](#file) | `stringArray` | | Build definition file |
| `--load` | | | Shorthand for `--set=*.output=type=docker` |
| [`--metadata-file`](#metadata-file) | `string` | | Write build result metadata to a file |
| [`--no-cache`](#no-cache) | | | Do not use cache when building the image |
| [`--print`](#print) | | | Print the options without building |
| [`--progress`](#progress) | `string` | `auto` | Set type of progress output (`auto`, `plain`, `tty`). Use plain to show container output |
| [`--provenance`](#provenance) | `string` | | Shorthand for `--set=*.attest=type=provenance` |
| [`--pull`](#pull) | | | Always attempt to pull all referenced images |
| `--push` | | | Shorthand for `--set=*.output=type=registry` |
| [`--sbom`](#sbom) | `string` | | Shorthand for `--set=*.attest=type=sbom` |
| [`--set`](#set) | `stringArray` | | Override target value (e.g., `targetpattern.key=value`) |


<!---MARKER_GEN_END-->
Expand Down Expand Up @@ -90,6 +90,76 @@ $ docker buildx bake -f docker-bake.dev.hcl db webapp-release
See the [Bake file reference](https://docs.docker.com/build/bake/reference/)
for more details.

### <a name="metadata-file"></a> Write build results metadata to a file (--metadata-file)

Similar to [`buildx build --metadata-file`](buildx_build.md#metadata-file) but
writes a map of results for each target such as:

```hcl
# docker-bake.hcl
group "default" {
targets = ["db", "webapp-dev"]
}
target "db" {
dockerfile = "Dockerfile.db"
tags = ["docker.io/username/db"]
}
target "webapp-dev" {
dockerfile = "Dockerfile.webapp"
tags = ["docker.io/username/webapp"]
}
```

```console
$ docker buildx bake --load --metadata-file metadata.json .
$ cat metadata.json
```

```json
{
"db": {
"buildx.build.provenance": {},
"buildx.build.ref": "mybuilder/mybuilder0/0fjb6ubs52xx3vygf6fgdl611",
"containerimage.config.digest": "sha256:2937f66a9722f7f4a2df583de2f8cb97fc9196059a410e7f00072fc918930e66",
"containerimage.descriptor": {
"annotations": {
"config.digest": "sha256:2937f66a9722f7f4a2df583de2f8cb97fc9196059a410e7f00072fc918930e66",
"org.opencontainers.image.created": "2022-02-08T21:28:03Z"
},
"digest": "sha256:19ffeab6f8bc9293ac2c3fdf94ebe28396254c993aea0b5a542cfb02e0883fa3",
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 506
},
"containerimage.digest": "sha256:19ffeab6f8bc9293ac2c3fdf94ebe28396254c993aea0b5a542cfb02e0883fa3"
},
"webapp-dev": {
"buildx.build.provenance": {},
"buildx.build.ref": "mybuilder/mybuilder0/kamngmcgyzebqxwu98b4lfv3n",
"containerimage.config.digest": "sha256:9651cc2b3c508f697c9c43b67b64c8359c2865c019e680aac1c11f4b875b67e0",
"containerimage.descriptor": {
"annotations": {
"config.digest": "sha256:9651cc2b3c508f697c9c43b67b64c8359c2865c019e680aac1c11f4b875b67e0",
"org.opencontainers.image.created": "2022-02-08T21:28:15Z"
},
"digest": "sha256:6d9ac9237a84afe1516540f40a0fafdc86859b2141954b4d643af7066d598b74",
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 506
},
"containerimage.digest": "sha256:6d9ac9237a84afe1516540f40a0fafdc86859b2141954b4d643af7066d598b74"
}
}
```

> **Note**
>
> Build record [provenance](https://docs.docker.com/build/attestations/slsa-provenance/#provenance-attestation-example)
> (`buildx.build.provenance`) is not included by default. Set the
> `BUILDX_METADATA_PROVENANCE` environment variable to include provenance in
> the metadata file. `min` will set minimal provenance information and `max`
> will set full provenance.
### <a name="no-cache"></a> Don't use cache when building the image (--no-cache)

Same as `build --no-cache`. Don't use cache when building the image.
Expand Down
Loading

0 comments on commit eba0b61

Please sign in to comment.