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