From a5967cb6022bdeb43290aede6981aeccbea6ab8a Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:14:43 +0200 Subject: [PATCH] imagetools: improve annotation handling Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- docs/reference/buildx_imagetools_create.md | 11 ++-- tests/imagetools.go | 30 +++++++++-- util/imagetools/create.go | 59 ++++++++++++++-------- util/imagetools/create_test.go | 34 +++++++++++++ 4 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 util/imagetools/create_test.go diff --git a/docs/reference/buildx_imagetools_create.md b/docs/reference/buildx_imagetools_create.md index 3e68790003bb..4b26dd3d0ba8 100644 --- a/docs/reference/buildx_imagetools_create.md +++ b/docs/reference/buildx_imagetools_create.md @@ -42,8 +42,9 @@ format in the output. ### Add annotations to an image (--annotation) -The `--annotation` flag lets you add annotations the image index, manifest, -and descriptors when creating a new image. +The `--annotation` flag lets you add annotations to the image index and +manifest descriptors when creating a new image. An annotation without a type +prefix is added to the image index. The following command creates a `foo/bar:latest` image with the `org.opencontainers.image.authors` annotation on the image index. @@ -62,7 +63,11 @@ $ docker buildx imagetools create \ > - `index:` > - `manifest-descriptor:` > -> It doesn't support annotating manifests or OCI layouts. +> It doesn't support annotating manifests or OCI layouts. Annotations also +> require the output to use the OCI image index media type; they aren't +> supported when the sources produce a Docker manifest list. When building +> source images with Buildx, use the `oci-mediatypes=true` output option to +> produce OCI media types. For more information about annotations, see [Annotations](https://docs.docker.com/build/building/annotations/). diff --git a/tests/imagetools.go b/tests/imagetools.go index a322934fa1fb..08421802b2db 100644 --- a/tests/imagetools.go +++ b/tests/imagetools.go @@ -867,7 +867,7 @@ func testImagetoolsFile(t *testing.T, sb integration.Sandbox) { require.Equal(t, len(sourceManifest.Layers), len(copiedManifest.Layers)) } -// testImagetoolsAnnotation verifies index and manifest annotations added by imagetools create. +// testImagetoolsAnnotation verifies index and manifest descriptor annotations added by imagetools create. func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { if !isDockerContainerWorker(sb) { t.Skip("only testing with docker-container worker, imagetools only runs on docker-container") @@ -895,7 +895,8 @@ func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { imagetoolsCmd := func(source []string) *exec.Cmd { args := []string{"imagetools", "create", "-t", target, "--annotation", "index:foo=bar", "--annotation", "index:bar=baz", - "--annotation", "manifest-descriptor:foo=bar", "--annotation", "manifest-descriptor[linux/amd64]:bar=baz"} + "--annotation", "untyped=value", "--annotation", "manifest-descriptor:foo=bar", + "--annotation", "manifest-descriptor[linux/amd64]:bar=baz"} args = append(args, source...) return buildxCmd(sb, withArgs(args...)) } @@ -924,9 +925,10 @@ func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { err = json.Unmarshal(dt, &idx) require.NoError(t, err) - require.Len(t, idx.Annotations, 2) + require.Len(t, idx.Annotations, 3) require.Equal(t, "bar", idx.Annotations["foo"]) require.Equal(t, "baz", idx.Annotations["bar"]) + require.Equal(t, "value", idx.Annotations["untyped"]) require.Len(t, idx.Manifests, 2) for _, mfst := range idx.Manifests { require.Equal(t, "bar", mfst.Annotations["foo"]) @@ -937,6 +939,28 @@ func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) { } } } + + cmd = buildxCmd(sb, withArgs("imagetools", "create", "--dry-run", "--annotation", "manifest:foo=bar", target)) + dt, err = cmd.CombinedOutput() + require.Error(t, err, string(dt)) + require.Contains(t, string(dt), `manifest annotations are not supported by imagetools create because it does not modify manifests; use "index:" or "manifest-descriptor:" instead`) + + dockerTarget := registry + "/buildx/imtools:docker" + out, err = buildCmd(sb, withArgs("--output", "type=registry,oci-mediatypes=false,name="+dockerTarget, "--platform=linux/amd64,linux/arm64", "--provenance=false", dir)) + require.NoError(t, err, string(out)) + + cmd = buildxCmd(sb, withArgs("imagetools", "inspect", dockerTarget, "--raw")) + dt, err = cmd.CombinedOutput() + require.NoError(t, err, string(dt)) + var dockerIdx ocispecs.Index + err = json.Unmarshal(dt, &dockerIdx) + require.NoError(t, err) + require.Equal(t, images.MediaTypeDockerSchema2ManifestList, dockerIdx.MediaType) + + cmd = buildxCmd(sb, withArgs("imagetools", "create", "--dry-run", "--annotation", "index:foo=bar", dockerTarget)) + dt, err = cmd.CombinedOutput() + require.Error(t, err, string(dt)) + require.Contains(t, string(dt), `annotations are not supported for Docker manifest lists; use "oci-mediatypes=true" when building the source images`) } // testImagetoolsMergeSources verifies create merges manifests from distinct source registries. diff --git a/util/imagetools/create.go b/util/imagetools/create.go index c13cae7ff9e1..104a0124e151 100644 --- a/util/imagetools/create.go +++ b/util/imagetools/create.go @@ -176,28 +176,9 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes mt = ocispecs.MediaTypeImageIndex } - // annotations are only allowed on OCI indexes - indexAnnotation := make(map[string]string) - if mt == ocispecs.MediaTypeImageIndex { - for k, v := range ann { - switch k.Type { - case exptypes.AnnotationIndex: - indexAnnotation[k.Key] = v - case exptypes.AnnotationManifestDescriptor: - for i := range descs { - if descs[i].Annotations == nil { - descs[i].Annotations = map[string]string{} - } - if k.Platform == nil || k.PlatformString() == platforms.Format(*descs[i].Platform) { - descs[i].Annotations[k.Key] = v - } - } - case exptypes.AnnotationManifest, "": - return nil, ocispecs.Descriptor{}, nil, errors.Errorf("%q annotations are not supported yet", k.Type) - case exptypes.AnnotationIndexDescriptor: - return nil, ocispecs.Descriptor{}, nil, errors.Errorf("%q annotations are invalid while creating an image", k.Type) - } - } + indexAnnotation, err := applyAnnotations(descs, ann, mt) + if err != nil { + return nil, ocispecs.Descriptor{}, nil, err } idxBytes, err := json.MarshalIndent(ocispecs.Index{ @@ -219,6 +200,40 @@ func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes }, sources, nil } +func applyAnnotations(descs []ocispecs.Descriptor, ann map[exptypes.AnnotationKey]string, mt string) (map[string]string, error) { + for k := range ann { + switch k.Type { + case "", exptypes.AnnotationIndex, exptypes.AnnotationManifestDescriptor: + case exptypes.AnnotationManifest: + return nil, errors.New(`manifest annotations are not supported by imagetools create because it does not modify manifests; use "index:" or "manifest-descriptor:" instead`) + case exptypes.AnnotationIndexDescriptor: + return nil, errors.Errorf("%q annotations are invalid while creating an image", k.Type) + } + } + + if len(ann) > 0 && mt != ocispecs.MediaTypeImageIndex { + return nil, errors.New(`annotations are not supported for Docker manifest lists; use "oci-mediatypes=true" when building the source images`) + } + + indexAnnotation := make(map[string]string) + for k, v := range ann { + switch k.Type { + case "", exptypes.AnnotationIndex: + indexAnnotation[k.Key] = v + case exptypes.AnnotationManifestDescriptor: + for i := range descs { + if descs[i].Annotations == nil { + descs[i].Annotations = map[string]string{} + } + if k.Platform == nil || k.PlatformString() == platforms.Format(*descs[i].Platform) { + descs[i].Annotations[k.Key] = v + } + } + } + } + return indexAnnotation, nil +} + func (r *Resolver) Push(ctx context.Context, ref *Location, desc ocispecs.Descriptor, dt []byte) error { ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto") if ref.IsOCILayout() { diff --git a/util/imagetools/create_test.go b/util/imagetools/create_test.go new file mode 100644 index 000000000000..25254a3ffbdd --- /dev/null +++ b/util/imagetools/create_test.go @@ -0,0 +1,34 @@ +package imagetools + +import ( + "testing" + + "github.com/containerd/containerd/v2/core/images" + "github.com/moby/buildkit/exporter/containerimage/exptypes" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" +) + +func TestApplyAnnotations(t *testing.T) { + t.Run("untyped defaults to index", func(t *testing.T) { + annotations, err := applyAnnotations(nil, map[exptypes.AnnotationKey]string{ + {Key: "org.example.key"}: "value", + }, ocispecs.MediaTypeImageIndex) + require.NoError(t, err) + require.Equal(t, map[string]string{"org.example.key": "value"}, annotations) + }) + + t.Run("manifest is unsupported", func(t *testing.T) { + _, err := applyAnnotations(nil, map[exptypes.AnnotationKey]string{ + {Type: exptypes.AnnotationManifest, Key: "org.example.key"}: "value", + }, ocispecs.MediaTypeImageIndex) + require.EqualError(t, err, `manifest annotations are not supported by imagetools create because it does not modify manifests; use "index:" or "manifest-descriptor:" instead`) + }) + + t.Run("Docker manifest list rejects annotations", func(t *testing.T) { + _, err := applyAnnotations(nil, map[exptypes.AnnotationKey]string{ + {Type: exptypes.AnnotationIndex, Key: "org.example.key"}: "value", + }, images.MediaTypeDockerSchema2ManifestList) + require.EqualError(t, err, `annotations are not supported for Docker manifest lists; use "oci-mediatypes=true" when building the source images`) + }) +}