Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions docs/reference/buildx_imagetools_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ format in the output.

### <a name="annotation"></a> 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.
Expand All @@ -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/).
Expand Down
30 changes: 27 additions & 3 deletions tests/imagetools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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...))
}
Expand Down Expand Up @@ -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"])
Expand All @@ -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.
Expand Down
59 changes: 37 additions & 22 deletions util/imagetools/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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() {
Expand Down
34 changes: 34 additions & 0 deletions util/imagetools/create_test.go
Original file line number Diff line number Diff line change
@@ -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`)
})
}
Loading