diff --git a/build/build.go b/build/build.go index 3fd2e581..42ec32a1 100644 --- a/build/build.go +++ b/build/build.go @@ -608,7 +608,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s } } - dt, desc, err := itpull.Combine(ctx, srcs, nil) + dt, desc, err := itpull.Combine(ctx, srcs, nil, false) if err != nil { return err } diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go index f05571b0..5239bd0f 100644 --- a/commands/imagetools/create.go +++ b/commands/imagetools/create.go @@ -29,6 +29,7 @@ type createOptions struct { dryrun bool actionAppend bool progress string + preferIndex bool } func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, args []string) error { @@ -153,7 +154,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg } } - dt, desc, err := r.Combine(ctx, srcs, in.annotations) + dt, desc, err := r.Combine(ctx, srcs, in.annotations, in.preferIndex) if err != nil { return err } @@ -283,6 +284,7 @@ func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command { flags.BoolVar(&options.actionAppend, "append", false, "Append to existing manifest") flags.StringVar(&options.progress, "progress", "auto", `Set type of progress output ("auto", "plain", "tty"). Use plain to show container output`) 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") return cmd } diff --git a/docs/reference/buildx_imagetools_create.md b/docs/reference/buildx_imagetools_create.md index cfce7478..1b3bc06a 100644 --- a/docs/reference/buildx_imagetools_create.md +++ b/docs/reference/buildx_imagetools_create.md @@ -9,15 +9,16 @@ Create a new image based on source images ### Options -| Name | Type | Default | Description | -|:---------------------------------|:--------------|:--------|:-----------------------------------------------------------------------------------------| -| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image | -| [`--append`](#append) | | | Append to existing manifest | -| [`--builder`](#builder) | `string` | | Override the configured builder instance | -| [`--dry-run`](#dry-run) | | | Show final image instead of pushing | -| [`-f`](#file), [`--file`](#file) | `stringArray` | | Read source descriptor from file | -| `--progress` | `string` | `auto` | Set type of progress output (`auto`, `plain`, `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) | | | Append to existing manifest | +| [`--builder`](#builder) | `string` | | Override the configured builder instance | +| [`--dry-run`](#dry-run) | | | Show final image instead of pushing | +| [`-f`](#file), [`--file`](#file) | `stringArray` | | Read source descriptor from file | +| `--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`, `plain`, `tty`). Use plain to show container output | +| [`-t`](#tag), [`--tag`](#tag) | `stringArray` | | Set reference for new image | @@ -26,8 +27,13 @@ Create a new image based on source images Create a new manifest list based on source manifests. The source manifests can be manifest lists or single platform distribution manifests and must already -exist in the registry where the new manifest is created. If only one source is -specified, create performs a carbon copy. +exist in the registry where the new manifest is created. + +If only one source is specified and that source is a manifest list or image index, +create performs a carbon copy. If one source is specified and that source is *not* +a list or index, the output will be a manifest list, however you can disable this +behavior with `--prefer-index=false` which attempts to preserve the source manifest +format in the output. ## Examples diff --git a/tests/imagetools.go b/tests/imagetools.go index b6d21b49..aa378bda 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -78,6 +78,19 @@ func testImagetoolsCopyManifest(t *testing.T, sb integration.Sandbox) { for i := range mfst.Layers { require.Equal(t, mfst.Layers[i].Digest, mfst2.Layers[i].Digest) } + + cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", target2+"-not-index", target)) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2+"-not-index", "--raw")) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + var idx3 ocispecs.Manifest + err = json.Unmarshal(dt, &idx3) + require.NoError(t, err) + require.Equal(t, images.MediaTypeDockerSchema2Manifest, idx3.MediaType) } func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { @@ -127,6 +140,24 @@ func testImagetoolsCopyIndex(t *testing.T, sb integration.Sandbox) { for i := range idx.Manifests { require.Equal(t, idx.Manifests[i].Digest, idx2.Manifests[i].Digest) } + + cmd = buildxCmd(sb, withArgs("imagetools", "create", "--prefer-index=false", "-t", target2+"-still-index", target)) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", target2+"-still-index", "--raw")) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + + var idx3 ocispecs.Index + err = json.Unmarshal(dt, &idx3) + require.NoError(t, err) + require.Equal(t, images.MediaTypeDockerSchema2ManifestList, idx3.MediaType) + + require.Equal(t, len(idx.Manifests), len(idx3.Manifests)) + for i := range idx.Manifests { + require.Equal(t, idx.Manifests[i].Digest, idx3.Manifests[i].Digest) + } } func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) { diff --git a/util/imagetools/create.go b/util/imagetools/create.go index d1e8dfaa..71853e07 100644 --- a/util/imagetools/create.go +++ b/util/imagetools/create.go @@ -29,7 +29,7 @@ type Source struct { Ref reference.Named } -func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string) ([]byte, ocispec.Descriptor, error) { +func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string, preferIndex bool) ([]byte, ocispec.Descriptor, error) { eg, ctx := errgroup.WithContext(ctx) dts := make([][]byte, len(srcs)) @@ -79,8 +79,16 @@ func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string) ([ // on single source, return original bytes if len(srcs) == 1 && len(ann) == 0 { - if mt := srcs[0].Desc.MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex { + switch srcs[0].Desc.MediaType { + // if the source is already an image index or manifest list, there is no need to consider the value + // of preferIndex since if set to true then the source is already in the preferred format, and if false + // it doesn't matter since we're not going to split it into separate manifests + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: return dts[0], srcs[0].Desc, nil + default: + if !preferIndex { + return dts[0], srcs[0].Desc, nil + } } }