mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-28 08:27:42 +08:00
Merge pull request #1965 from mqasimsarfraz/qasim/oci-annotations
This commit is contained in:
commit
e5cee892ed
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 |
|
||||
|
@ -2,6 +2,7 @@ package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/platforms"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
|
||||
var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testImagetoolsInspectAndFilter,
|
||||
testImagetoolsAnnotation,
|
||||
}
|
||||
|
||||
func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) {
|
||||
@ -21,18 +23,7 @@ func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) {
|
||||
t.Skip("imagetools tests are not driver specific and only run on docker-container")
|
||||
}
|
||||
|
||||
dockerfile := []byte(`
|
||||
FROM scratch
|
||||
ARG TARGETARCH
|
||||
COPY foo-${TARGETARCH} /foo
|
||||
`)
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo-amd64", []byte("foo-amd64"), 0600),
|
||||
fstest.CreateFile("foo-arm64", []byte("foo-arm64"), 0600),
|
||||
)
|
||||
|
||||
dir := createDockerfile(t)
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
@ -77,3 +68,89 @@ func testImagetoolsInspectAndFilter(t *testing.T, sb integration.Sandbox) {
|
||||
require.Equal(t, idx.Manifests[1].Digest, idx2.Manifests[0].Digest)
|
||||
require.Equal(t, platforms.Format(*idx.Manifests[1].Platform), platforms.Format(*idx2.Manifests[0].Platform))
|
||||
}
|
||||
|
||||
func testImagetoolsAnnotation(t *testing.T, sb integration.Sandbox) {
|
||||
if sb.Name() != "docker-container" {
|
||||
t.Skip("imagetools tests are not driver specific and only run on docker-container")
|
||||
}
|
||||
|
||||
dir := createDockerfile(t)
|
||||
registry, err := sb.NewRegistry()
|
||||
if errors.Is(err, integration.ErrRequirements) {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
target := registry + "/buildx/imtools:latest"
|
||||
|
||||
out, err := buildCmd(sb, withArgs("--output", "type=registry,oci-mediatypes=true,name="+target, "--platform=linux/amd64,linux/arm64", "--provenance=false", dir))
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
cmd := buildxCmd(sb, withArgs("imagetools", "inspect", target, "--raw"))
|
||||
dt, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
var idx ocispecs.Index
|
||||
err = json.Unmarshal(dt, &idx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, idx.Annotations)
|
||||
|
||||
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"}
|
||||
args = append(args, source...)
|
||||
return buildxCmd(sb, withArgs(args...))
|
||||
}
|
||||
sources := [][]string{
|
||||
{
|
||||
target,
|
||||
},
|
||||
{
|
||||
target + "@" + string(idx.Manifests[0].Digest),
|
||||
target + "@" + string(idx.Manifests[1].Digest),
|
||||
},
|
||||
}
|
||||
for _, source := range sources {
|
||||
cmd = imagetoolsCmd(source)
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
newTarget := registry + "/buildx/imtools:annotations"
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "create", "-t", newTarget, target))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
cmd = buildxCmd(sb, withArgs("imagetools", "inspect", newTarget, "--raw"))
|
||||
dt, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(dt))
|
||||
|
||||
err = json.Unmarshal(dt, &idx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, idx.Annotations, 2)
|
||||
require.Equal(t, "bar", idx.Annotations["foo"])
|
||||
require.Equal(t, "baz", idx.Annotations["bar"])
|
||||
require.Len(t, idx.Manifests, 2)
|
||||
for _, mfst := range idx.Manifests {
|
||||
require.Equal(t, "bar", mfst.Annotations["foo"])
|
||||
if platforms.Format(*mfst.Platform) == "linux/amd64" {
|
||||
require.Equal(t, "baz", mfst.Annotations["bar"])
|
||||
} else {
|
||||
require.Empty(t, mfst.Annotations["bar"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createDockerfile(t *testing.T) string {
|
||||
dockerfile := []byte(`
|
||||
FROM scratch
|
||||
ARG TARGETARCH
|
||||
COPY foo-${TARGETARCH} /foo
|
||||
`)
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
fstest.CreateFile("foo-amd64", []byte("foo-amd64"), 0600),
|
||||
fstest.CreateFile("foo-arm64", []byte("foo-arm64"), 0600),
|
||||
)
|
||||
return dir
|
||||
}
|
||||
|
@ -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 <type>:<key>=<value>, 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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user