diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go index df28603daad0..bd949c929c34 100644 --- a/commands/imagetools/create.go +++ b/commands/imagetools/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "sort" "strings" "github.com/containerd/containerd/v2/core/remotes" @@ -16,7 +17,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 +37,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 { @@ -76,11 +80,16 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg } repos := map[string]struct{}{} - for _, t := range tags { repos[t.Name()] = struct{}{} } + repoNames := make([]string, 0, len(repos)) + for repo := range repos { + repoNames = append(repoNames, repo) + } + sort.Strings(repoNames) + sourceRefs := false for _, s := range srcs { if s.Ref != nil { @@ -241,6 +250,15 @@ 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.ExporterImageDescriptorKey: desc, + exptypes.ExporterImageNameKey: strings.Join(repoNames, ","), + }); err != nil { + return err + } + } + return err } @@ -348,6 +366,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 +386,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..3e68790003bb 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,28 @@ 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 user/app:latest -f image1 -f image2 --metadata-file metadata.json +$ cat metadata.json +``` + +```json +{ + "containerimage.descriptor": { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:19ffeab6f8bc9293ac2c3fdf94ebe28396254c993aea0b5a542cfb02e0883fa3", + "size": 4654 + }, + "image.name": "docker.io/user/app" +} +``` + ### Set reference for new image (-t, --tag) ```text diff --git a/tests/imagetools.go b/tests/imagetools.go index 50055bceb85c..e947244fe6cf 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -2,7 +2,10 @@ package tests import ( "encoding/json" + "os" "os/exec" + "path" + "path/filepath" "testing" "github.com/containerd/containerd/v2/core/images" @@ -50,10 +53,23 @@ 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 { + ImageDescriptor ocispecs.Descriptor `json:"containerimage.descriptor"` + ImageName string `json:"image.name"` + } + var md mdT + err = json.Unmarshal(mddt, &md) + require.NoError(t, err) + require.NotEmpty(t, md.ImageDescriptor) + require.Equal(t, registry2+"/buildx/imtools2-manifest", md.ImageName) + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt)) @@ -123,10 +139,23 @@ 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 { + ImageDescriptor ocispecs.Descriptor `json:"containerimage.descriptor"` + ImageName string `json:"image.name"` + } + var md mdT + err = json.Unmarshal(mddt, &md) + require.NoError(t, err) + require.NotEmpty(t, md.ImageDescriptor) + require.Equal(t, registry2+"/buildx/imtools2", md.ImageName) + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2, "--raw")) dt, err = cmd.CombinedOutput() require.NoError(t, err, string(dt))