From 18894a8e3ac4fa4c4e47e905e0617cc289cfdab7 Mon Sep 17 00:00:00 2001 From: Qasim Sarfraz Date: Thu, 20 Jul 2023 16:28:52 +0200 Subject: [PATCH] allow annotations for OCI image index Signed-off-by: Qasim Sarfraz --- build/build.go | 2 +- commands/imagetools/create.go | 21 +++++- docs/reference/buildx_imagetools_create.md | 1 + util/imagetools/create.go | 84 +++++++++++++++++++++- 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/build/build.go b/build/build.go index 1cd20ee2..8812718e 100644 --- a/build/build.go +++ b/build/build.go @@ -1101,7 +1101,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s } } - dt, desc, err := itpull.Combine(ctx, srcs) + dt, desc, err := itpull.Combine(ctx, srcs, nil) if err != nil { return err } diff --git a/commands/imagetools/create.go b/commands/imagetools/create.go index 15ecd889..0e8e9d49 100644 --- a/commands/imagetools/create.go +++ b/commands/imagetools/create.go @@ -25,6 +25,7 @@ type createOptions struct { builder string files []string tags []string + annotations []string dryrun bool actionAppend bool progress string @@ -82,6 +83,11 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error { return errors.Errorf("no repositories specified, please set a reference in tag or source") } + ann, err := parseAnnotations(in.annotations) + if err != nil { + return err + } + var defaultRepo *string if len(repos) == 1 { for repo := range repos { @@ -154,7 +160,7 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error { } } - dt, desc, err := r.Combine(ctx, srcs) + dt, desc, err := r.Combine(ctx, srcs, ann) if err != nil { return err } @@ -264,6 +270,18 @@ func parseSource(in string) (*imagetools.Source, error) { return &s, nil } +func parseAnnotations(in []string) (map[string]string, error) { + out := make(map[string]string) + for _, i := range in { + kv := strings.SplitN(i, "=", 2) + if len(kv) != 2 { + return nil, errors.Errorf("invalid annotation %q, expected key=value", in) + } + out[kv[0]] = kv[1] + } + return out, nil +} + func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command { var options createOptions @@ -283,6 +301,7 @@ func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command { flags.BoolVar(&options.dryrun, "dry-run", false, "Show final image instead of pushing") 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") return cmd } diff --git a/docs/reference/buildx_imagetools_create.md b/docs/reference/buildx_imagetools_create.md index 3b2f6487..e5671f0f 100644 --- a/docs/reference/buildx_imagetools_create.md +++ b/docs/reference/buildx_imagetools_create.md @@ -11,6 +11,7 @@ Create a new image based on source images | Name | Type | Default | Description | |:---------------------------------|:--------------|:--------|:-----------------------------------------------------------------------------------------| +| `--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 | diff --git a/util/imagetools/create.go b/util/imagetools/create.go index 3b1057bc..dca0ae1f 100644 --- a/util/imagetools/create.go +++ b/util/imagetools/create.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "net/url" + "regexp" "strings" "github.com/containerd/containerd/content" @@ -13,6 +14,7 @@ import ( "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/remotes" "github.com/docker/distribution/reference" + "github.com/moby/buildkit/exporter/containerimage/exptypes" "github.com/moby/buildkit/util/contentutil" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" @@ -26,7 +28,7 @@ type Source struct { Ref reference.Named } -func (r *Resolver) Combine(ctx context.Context, srcs []*Source) ([]byte, ocispec.Descriptor, error) { +func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[string]string) ([]byte, ocispec.Descriptor, error) { eg, ctx := errgroup.WithContext(ctx) dts := make([][]byte, len(srcs)) @@ -75,7 +77,7 @@ func (r *Resolver) Combine(ctx context.Context, srcs []*Source) ([]byte, ocispec } // on single source, return original bytes - if len(srcs) == 1 { + if len(srcs) == 1 && len(ann) == 0 { if mt := srcs[0].Desc.MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex { return dts[0], srcs[0].Desc, nil } @@ -138,12 +140,39 @@ func (r *Resolver) Combine(ctx context.Context, srcs []*Source) ([]byte, ocispec mt = ocispec.MediaTypeImageIndex } + // annotations are only allowed on OCI indexes + indexAnnotation := make(map[string]string) + if mt == ocispec.MediaTypeImageIndex { + annotations, err := parseAnnotations(ann) + if err != nil { + return nil, ocispec.Descriptor{}, err + } + if len(annotations[exptypes.AnnotationIndex]) > 0 { + for k, v := range annotations[exptypes.AnnotationIndex] { + indexAnnotation[k.Key] = v + } + } + if len(annotations[exptypes.AnnotationManifestDescriptor]) > 0 { + for i := 0; i < len(newDescs); i++ { + if newDescs[i].Annotations == nil { + newDescs[i].Annotations = map[string]string{} + } + for k, v := range annotations[exptypes.AnnotationManifestDescriptor] { + if k.Platform == nil || k.PlatformString() == platforms.Format(*newDescs[i].Platform) { + newDescs[i].Annotations[k.Key] = v + } + } + } + } + } + idxBytes, err := json.MarshalIndent(ocispec.Index{ MediaType: mt, Versioned: specs.Versioned{ SchemaVersion: 2, }, - Manifests: newDescs, + Manifests: newDescs, + Annotations: indexAnnotation, }, "", " ") if err != nil { return nil, ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal index") @@ -266,3 +295,52 @@ func detectMediaType(dt []byte) (string, error) { return images.MediaTypeDockerSchema2ManifestList, nil } + +func parseAnnotations(ann map[string]string) (map[string]map[exptypes.AnnotationKey]string, error) { + // TODO: use buildkit's annotation parser once it supports setting custom prefix and ":" separator + annotationRegexp := regexp.MustCompile(`^([a-z-]+)(?:\[([A-Za-z0-9_/-]+)\])?:(\S+)$`) + indexAnnotations := make(map[exptypes.AnnotationKey]string) + manifestDescriptorAnnotations := make(map[exptypes.AnnotationKey]string) + for k, v := range ann { + groups := annotationRegexp.FindStringSubmatch(k) + if groups == nil { + return nil, errors.Errorf("invalid annotation format, expected :=, got %q", k) + } + + typ, platform, key := groups[1], groups[2], groups[3] + var ociPlatform *ocispec.Platform + if platform != "" { + p, err := platforms.Parse(platform) + if err != nil { + return nil, errors.Wrapf(err, "invalid platform %q", platform) + } + ociPlatform = &p + } + switch typ { + case exptypes.AnnotationIndex: + ak := exptypes.AnnotationKey{ + Type: typ, + Platform: ociPlatform, + Key: key, + } + indexAnnotations[ak] = v + case exptypes.AnnotationManifestDescriptor: + ak := exptypes.AnnotationKey{ + Type: typ, + Platform: ociPlatform, + Key: key, + } + manifestDescriptorAnnotations[ak] = v + case exptypes.AnnotationManifest: + return nil, errors.Errorf("%q annotations are not supported yet", typ) + case exptypes.AnnotationIndexDescriptor: + return nil, errors.Errorf("%q annotations are invalid while creating an image", typ) + default: + return nil, errors.Errorf("unknown annotation type %q", typ) + } + } + return map[string]map[exptypes.AnnotationKey]string{ + exptypes.AnnotationIndex: indexAnnotations, + exptypes.AnnotationManifestDescriptor: manifestDescriptorAnnotations, + }, nil +}