Skip to content

Commit

Permalink
Merge pull request #2879 from jedevc/annotations-support
Browse files Browse the repository at this point in the history
Add annotations support to output
  • Loading branch information
tonistiigi committed Jun 17, 2022
2 parents b1720cf + cde312a commit a6a114a
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 292 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -242,6 +242,10 @@ Keys supported by image output:
* `buildinfo=true`: inline build info in [image config](docs/build-repro.md#image-config) (default `true`).
* `buildinfo-attrs=true`: inline build info attributes in [image config](docs/build-repro.md#image-config) (default `false`).
* `store=true`: stores the result images to the worker's (e.g. containerd) image store as well as ensures that the image has all blobs in the content store (default `true`). Ignored if the worker doesn't have image store (e.g. OCI worker).
* `annotation.key=value`: attaches an annotation with the respective `key` and `value` to the built image.
* Using the extended syntaxes, `annotation-<type>.key=value`, `annotation[<platform>].key=value` and both combined with `annotation-<type>[<platform>].key=value`, allows configuring exactly where to attach the annotation.
* `<type>` specifies what object to attach to, and can be any of `manifest` (the default), `manifest-descriptor`, `index` and `index-descriptor`
* `<platform>` specifies which objects to attach to (by default, all), and is the same key passed into the `platform` opt, see [`docs/multi-platform.md`](docs/multi-platform.md).

If credentials are required, `buildctl` will attempt to read Docker configuration file `$DOCKER_CONFIG/config.json`.
`$DOCKER_CONFIG` defaults to `~/.docker`.
Expand Down
217 changes: 217 additions & 0 deletions client/client_test.go
Expand Up @@ -165,6 +165,7 @@ func TestIntegration(t *testing.T) {
testStargzLazyRegistryCacheImportExport,
testCallInfo,
testPullWithLayerLimit,
testExportAnnotations,
)
tests = append(tests, diffOpTestCases()...)
integration.Run(t, tests, mirrors)
Expand Down Expand Up @@ -5982,6 +5983,222 @@ func testCallInfo(t *testing.T, sb integration.Sandbox) {
require.NoError(t, err)
}

func testExportAnnotations(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

amd64 := platforms.MustParse("linux/amd64")
arm64 := platforms.MustParse("linux/arm64")
ps := []ocispecs.Platform{amd64, arm64}

frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res := gateway.NewResult()
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(ps)),
}
for i, p := range ps {
st := llb.Scratch().File(
llb.Mkfile("platform", 0600, []byte(platforms.Format(p))),
)

def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}

r, err := c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}

ref, err := r.SingleRef()
if err != nil {
return nil, err
}

_, err = ref.ToState()
if err != nil {
return nil, err
}

k := platforms.Format(p)
res.AddRef(k, ref)

expPlatforms.Platforms[i] = exptypes.Platform{
ID: k,
Platform: p,
}
}
dt, err := json.Marshal(expPlatforms)
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)

res.AddMeta(exptypes.AnnotationIndexKey("gi"), []byte("generic index"))
res.AddMeta(exptypes.AnnotationIndexDescriptorKey("gid"), []byte("generic index descriptor"))
res.AddMeta(exptypes.AnnotationManifestKey(nil, "gm"), []byte("generic manifest"))
res.AddMeta(exptypes.AnnotationManifestDescriptorKey(nil, "gmd"), []byte("generic manifest descriptor"))
res.AddMeta(exptypes.AnnotationManifestKey(&amd64, "m"), []byte("amd64 manifest"))
res.AddMeta(exptypes.AnnotationManifestKey(&arm64, "m"), []byte("arm64 manifest"))
res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&amd64, "md"), []byte("amd64 manifest descriptor"))
res.AddMeta(exptypes.AnnotationManifestDescriptorKey(&arm64, "md"), []byte("arm64 manifest descriptor"))
res.AddMeta(exptypes.AnnotationKey{Key: "gd"}.String(), []byte("generic default"))

return res, nil
}

// testing for image exporter

target := "testannotations:latest"

_, err = c.Build(sb.Context(), SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterImage,
Attrs: map[string]string{
"name": target,
"annotation-index.gio": "generic index opt",
"annotation-manifest.gmo": "generic manifest opt",
"annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt",
"annotation-manifest[linux/amd64].mo": "amd64 manifest opt",
"annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt",
"annotation-manifest[linux/arm64].mo": "arm64 manifest opt",
"annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt",
},
},
},
}, "", frontend, nil)
require.NoError(t, err)

ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
cdAddress := sb.ContainerdAddress()
if cdAddress != "" {
client, err := newContainerd(cdAddress)
require.NoError(t, err)
defer client.Close()

img, err := client.GetImage(ctx, target)
require.NoError(t, err)

var index ocispecs.Index
indexBytes, err := content.ReadBlob(ctx, client.ContentStore(), img.Target())
require.NoError(t, err)
require.NoError(t, json.Unmarshal(indexBytes, &index))

require.Equal(t, "generic index", index.Annotations["gi"])
require.Equal(t, "generic index opt", index.Annotations["gio"])
for _, desc := range index.Manifests {
require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"])
require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"])
switch {
case platforms.Only(amd64).Match(*desc.Platform):
require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"])
require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"])
case platforms.Only(arm64).Match(*desc.Platform):
require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"])
require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"])
default:
require.Fail(t, "unrecognized platform")
}
}

mfst, err := images.Manifest(ctx, client.ContentStore(), img.Target(), platforms.Only(amd64))
require.NoError(t, err)
require.Equal(t, "generic default", mfst.Annotations["gd"])
require.Equal(t, "generic manifest", mfst.Annotations["gm"])
require.Equal(t, "generic manifest opt", mfst.Annotations["gmo"])
require.Equal(t, "amd64 manifest", mfst.Annotations["m"])
require.Equal(t, "amd64 manifest opt", mfst.Annotations["mo"])

mfst, err = images.Manifest(ctx, client.ContentStore(), img.Target(), platforms.Only(arm64))
require.NoError(t, err)
require.Equal(t, "generic manifest", mfst.Annotations["gm"])
require.Equal(t, "generic manifest opt", mfst.Annotations["gmo"])
require.Equal(t, "arm64 manifest", mfst.Annotations["m"])
require.Equal(t, "arm64 manifest opt", mfst.Annotations["mo"])
}

// testing for oci exporter

destDir, err := os.MkdirTemp("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)

out := filepath.Join(destDir, "out.tar")
outW, err := os.Create(out)
require.NoError(t, err)

_, err = c.Build(sb.Context(), SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterOCI,
Output: fixedWriteCloser(outW),
Attrs: map[string]string{
"annotation-index.gio": "generic index opt",
"annotation-index-descriptor.gido": "generic index descriptor opt",
"annotation-manifest.gmo": "generic manifest opt",
"annotation-manifest-descriptor.gmdo": "generic manifest descriptor opt",
"annotation-manifest[linux/amd64].mo": "amd64 manifest opt",
"annotation-manifest-descriptor[linux/amd64].mdo": "amd64 manifest descriptor opt",
"annotation-manifest[linux/arm64].mo": "arm64 manifest opt",
"annotation-manifest-descriptor[linux/arm64].mdo": "arm64 manifest descriptor opt",
},
},
},
}, "", frontend, nil)
require.NoError(t, err)

dt, err := os.ReadFile(out)
require.NoError(t, err)

m, err := testutil.ReadTarToMap(dt, false)
require.NoError(t, err)

var layout ocispecs.Index
err = json.Unmarshal(m["index.json"].Data, &layout)
require.Equal(t, "generic index descriptor", layout.Manifests[0].Annotations["gid"])
require.Equal(t, "generic index descriptor opt", layout.Manifests[0].Annotations["gido"])
require.NoError(t, err)

var index ocispecs.Index
err = json.Unmarshal(m["blobs/sha256/"+layout.Manifests[0].Digest.Hex()].Data, &index)
require.Equal(t, "generic index", index.Annotations["gi"])
require.Equal(t, "generic index opt", index.Annotations["gio"])
require.NoError(t, err)

for _, desc := range index.Manifests {
var mfst ocispecs.Manifest
err = json.Unmarshal(m["blobs/sha256/"+desc.Digest.Hex()].Data, &mfst)
require.NoError(t, err)

require.Equal(t, "generic default", mfst.Annotations["gd"])
require.Equal(t, "generic manifest", mfst.Annotations["gm"])
require.Equal(t, "generic manifest descriptor", desc.Annotations["gmd"])
require.Equal(t, "generic manifest opt", mfst.Annotations["gmo"])
require.Equal(t, "generic manifest descriptor opt", desc.Annotations["gmdo"])

switch {
case platforms.Only(amd64).Match(*desc.Platform):
require.Equal(t, "amd64 manifest", mfst.Annotations["m"])
require.Equal(t, "amd64 manifest descriptor", desc.Annotations["md"])
require.Equal(t, "amd64 manifest opt", mfst.Annotations["mo"])
require.Equal(t, "amd64 manifest descriptor opt", desc.Annotations["mdo"])
case platforms.Only(arm64).Match(*desc.Platform):
require.Equal(t, "arm64 manifest", mfst.Annotations["m"])
require.Equal(t, "arm64 manifest descriptor", desc.Annotations["md"])
require.Equal(t, "arm64 manifest opt", mfst.Annotations["mo"])
require.Equal(t, "arm64 manifest descriptor opt", desc.Annotations["mdo"])
default:
require.Fail(t, "unrecognized platform")
}
}
}

func tmpdir(appliers ...fstest.Applier) (string, error) {
tmpdir, err := os.MkdirTemp("", "buildkit-client")
if err != nil {
Expand Down
131 changes: 131 additions & 0 deletions exporter/containerimage/annotations.go
@@ -0,0 +1,131 @@
package containerimage

import (
"fmt"

ocispecs "github.com/opencontainers/image-spec/specs-go/v1"

"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
)

type Annotations struct {
Index map[string]string
IndexDescriptor map[string]string
Manifest map[string]string
ManifestDescriptor map[string]string
}

// AnnotationsGroup is a map of annotations keyed by the reference key
type AnnotationsGroup map[string]*Annotations

func ParseAnnotations(data map[string][]byte) (AnnotationsGroup, map[string][]byte, error) {
ag := make(AnnotationsGroup)
rest := make(map[string][]byte)

for k, v := range data {
a, ok, err := exptypes.ParseAnnotationKey(k)
if !ok {
rest[k] = v
continue
}
if err != nil {
return nil, nil, err
}

p := a.PlatformString()

if ag[p] == nil {
ag[p] = &Annotations{
IndexDescriptor: make(map[string]string),
Index: make(map[string]string),
Manifest: make(map[string]string),
ManifestDescriptor: make(map[string]string),
}
}

switch a.Type {
case exptypes.AnnotationIndex:
ag[p].Index[a.Key] = string(v)
case exptypes.AnnotationIndexDescriptor:
ag[p].IndexDescriptor[a.Key] = string(v)
case exptypes.AnnotationManifest:
ag[p].Manifest[a.Key] = string(v)
case exptypes.AnnotationManifestDescriptor:
ag[p].ManifestDescriptor[a.Key] = string(v)
default:
return nil, nil, fmt.Errorf("unrecognized annotation type %s", a.Type)
}
}
return ag, rest, nil
}

func (ag AnnotationsGroup) Platform(p *ocispecs.Platform) *Annotations {
res := &Annotations{
IndexDescriptor: make(map[string]string),
Index: make(map[string]string),
Manifest: make(map[string]string),
ManifestDescriptor: make(map[string]string),
}

ps := []string{""}
if p != nil {
ps = append(ps, platforms.Format(*p))
}

for _, a := range ag {
for k, v := range a.Index {
res.Index[k] = v
}
for k, v := range a.IndexDescriptor {
res.IndexDescriptor[k] = v
}
}
for _, pk := range ps {
if _, ok := ag[pk]; !ok {
continue
}

for k, v := range ag[pk].Manifest {
res.Manifest[k] = v
}
for k, v := range ag[pk].ManifestDescriptor {
res.ManifestDescriptor[k] = v
}
}
return res
}

func (ag AnnotationsGroup) Merge(other AnnotationsGroup) AnnotationsGroup {
if other == nil {
return ag
}

for k, v := range other {
if _, ok := ag[k]; ok {
ag[k].merge(v)
} else {
ag[k] = v
}
}
return ag
}

func (a *Annotations) merge(other *Annotations) {
if other == nil {
return
}

for k, v := range other.Index {
a.Index[k] = v
}
for k, v := range other.IndexDescriptor {
a.IndexDescriptor[k] = v
}
for k, v := range other.Manifest {
a.Manifest[k] = v
}
for k, v := range other.ManifestDescriptor {
a.ManifestDescriptor[k] = v
}
}

0 comments on commit a6a114a

Please sign in to comment.