diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go
index df28603daad0..c9e86600fd91 100644
--- a/commands/imagetools/create.go
+++ b/commands/imagetools/create.go
@@ -16,7 +16,9 @@ import (
"github.com/docker/buildx/util/imagetools"
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
+ "github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/util/progress/progressui"
+ "github.com/moby/sys/atomicwriter"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
@@ -34,6 +36,7 @@ type createOptions struct {
progress string
preferIndex bool
platforms []string
+ metadataFile string
}
func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, args []string) error {
@@ -241,6 +244,14 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
err = err1
}
+ if err == nil && len(in.metadataFile) > 0 {
+ if err := writeMetadataFile(in.metadataFile, map[string]any{
+ exptypes.ExporterImageDigestKey: desc.Digest.String(),
+ }); err != nil {
+ return err
+ }
+ }
+
return err
}
@@ -348,6 +359,7 @@ func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command {
flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")
flags.BoolVar(&options.preferIndex, "prefer-index", true, "When only a single source is specified, prefer outputting an image index or manifest list instead of performing a carbon copy")
flags.StringArrayVarP(&options.platforms, "platform", "p", []string{}, "Filter specified platforms of target image")
+ flags.StringVar(&options.metadataFile, "metadata-file", "", "Write create result metadata to a file")
return cmd
}
@@ -367,3 +379,11 @@ func mergeDesc(d1, d2 ocispecs.Descriptor) (ocispecs.Descriptor, error) {
}
return d1, nil
}
+
+func writeMetadataFile(filename string, dt any) error {
+ b, err := json.MarshalIndent(dt, "", " ")
+ if err != nil {
+ return err
+ }
+ return atomicwriter.WriteFile(filename, b, 0o644)
+}
diff --git a/docs/reference/buildx_imagetools_create.md b/docs/reference/buildx_imagetools_create.md
index c69fb31afcde..009cf13172eb 100644
--- a/docs/reference/buildx_imagetools_create.md
+++ b/docs/reference/buildx_imagetools_create.md
@@ -9,18 +9,19 @@ Create a new image based on source images
### Options
-| Name | Type | Default | Description |
-|:---------------------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------------------------|
-| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image |
-| [`--append`](#append) | `bool` | | Append to existing manifest |
-| [`--builder`](#builder) | `string` | | Override the configured builder instance |
-| `-D`, `--debug` | `bool` | | Enable debug logging |
-| [`--dry-run`](#dry-run) | `bool` | | Show final image instead of pushing |
-| [`-f`](#file), [`--file`](#file) | `stringArray` | | Read source descriptor from file |
-| `-p`, `--platform` | `stringArray` | | Filter specified platforms of target image |
-| `--prefer-index` | `bool` | `true` | When only a single source is specified, prefer outputting an image index or manifest list instead of performing a carbon copy |
-| `--progress` | `string` | `auto` | Set type of progress output (`auto`, `none`, `plain`, `rawjson`, `tty`). Use plain to show container output |
-| [`-t`](#tag), [`--tag`](#tag) | `stringArray` | | Set reference for new image |
+| Name | Type | Default | Description |
+|:------------------------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------------------------|
+| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image |
+| [`--append`](#append) | `bool` | | Append to existing manifest |
+| [`--builder`](#builder) | `string` | | Override the configured builder instance |
+| `-D`, `--debug` | `bool` | | Enable debug logging |
+| [`--dry-run`](#dry-run) | `bool` | | Show final image instead of pushing |
+| [`-f`](#file), [`--file`](#file) | `stringArray` | | Read source descriptor from file |
+| [`--metadata-file`](#metadata-file) | `string` | | Write create result metadata to a file |
+| `-p`, `--platform` | `stringArray` | | Filter specified platforms of target image |
+| `--prefer-index` | `bool` | `true` | When only a single source is specified, prefer outputting an image index or manifest list instead of performing a carbon copy |
+| `--progress` | `string` | `auto` | Set type of progress output (`auto`, `none`, `plain`, `rawjson`, `tty`). Use plain to show container output |
+| [`-t`](#tag), [`--tag`](#tag) | `stringArray` | | Set reference for new image |
@@ -100,6 +101,23 @@ The descriptor in the file is merged with existing descriptor in the registry if
The supported fields for the descriptor are defined in [OCI spec](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#properties) .
+### Write create result metadata to a file (--metadata-file)
+
+To output metadata such as the image digest, pass the `--metadata-file` flag.
+The metadata will be written as a JSON object to the specified file. The
+directory of the specified file must already exist and be writable.
+
+```console
+$ docker buildx imagetools create -t tonistiigi/myapp -f image1 -f image2 --metadata-file metadata.json
+$ cat metadata.json
+```
+
+```json
+{
+ "containerimage.digest": "sha256:19ffeab6f8bc9293ac2c3fdf94ebe28396254c993aea0b5a542cfb02e0883fa3"
+}
+```
+
### Set reference for new image (-t, --tag)
```text
diff --git a/tests/imagetools.go b/tests/imagetools.go
index 50055bceb85c..9e92981ebcc1 100644
--- a/tests/imagetools.go
+++ b/tests/imagetools.go
@@ -2,13 +2,17 @@ package tests
import (
"encoding/json"
+ "os"
"os/exec"
+ "path"
+ "path/filepath"
"testing"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/continuity/fs/fstest"
"github.com/containerd/platforms"
"github.com/moby/buildkit/util/testutil/integration"
+ "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
@@ -50,10 +54,24 @@ func testImagetoolsCopyManifest(t *testing.T, sb integration.Sandbox) {
require.NoError(t, err)
target2 := registry2 + "/buildx/imtools2-manifest:latest"
- cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target2, target))
+ cmd = buildxCmd(sb, withArgs("imagetools", "create", "--metadata-file", path.Join(dir, "md.json"), "-t", target2, target))
dt, err = cmd.CombinedOutput()
require.NoError(t, err, string(dt))
+ mddt, err := os.ReadFile(filepath.Join(dir, "md.json"))
+ require.NoError(t, err)
+
+ type mdT struct {
+ ImageDigest string `json:"containerimage.digest"`
+ }
+ var md mdT
+ err = json.Unmarshal(mddt, &md)
+ require.NoError(t, err)
+
+ require.NotEmpty(t, md.ImageDigest)
+ _, err = digest.Parse(md.ImageDigest)
+ require.NoError(t, err)
+
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw"))
dt, err = cmd.CombinedOutput()
require.NoError(t, err, string(dt))
@@ -123,10 +141,24 @@ func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) {
require.NoError(t, err)
target2 := registry2 + "/buildx/imtools2:latest"
- cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", target2, target))
+ cmd = buildxCmd(sb, withArgs("imagetools", "create", "--metadata-file", path.Join(dir, "md.json"), "-t", target2, target))
dt, err = cmd.CombinedOutput()
require.NoError(t, err, string(dt))
+ mddt, err := os.ReadFile(filepath.Join(dir, "md.json"))
+ require.NoError(t, err)
+
+ type mdT struct {
+ ImageDigest string `json:"containerimage.digest"`
+ }
+ var md mdT
+ err = json.Unmarshal(mddt, &md)
+ require.NoError(t, err)
+
+ require.NotEmpty(t, md.ImageDigest)
+ _, err = digest.Parse(md.ImageDigest)
+ require.NoError(t, err)
+
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw"))
dt, err = cmd.CombinedOutput()
require.NoError(t, err, string(dt))