Skip to content

Commit

Permalink
Merge pull request docker#2020 from jedevc/build-annotation-flags
Browse files Browse the repository at this point in the history
  • Loading branch information
jedevc authored Sep 11, 2023
2 parents f6b7a3c + 0138f2a commit 66e6dab
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 80 deletions.
17 changes: 17 additions & 0 deletions bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ type Target struct {
// Inherits is the only field that cannot be overridden with --set
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`

Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
Expand Down Expand Up @@ -620,6 +621,7 @@ var _ hclparser.WithEvalContexts = &Group{}
var _ hclparser.WithGetName = &Group{}

func (t *Target) normalize() {
t.Annotations = removeDupes(t.Annotations)
t.Attest = removeAttestDupes(t.Attest)
t.Tags = removeDupes(t.Tags)
t.Secrets = removeDupes(t.Secrets)
Expand Down Expand Up @@ -680,6 +682,9 @@ func (t *Target) Merge(t2 *Target) {
if t2.Target != nil {
t.Target = t2.Target
}
if t2.Annotations != nil { // merge
t.Annotations = append(t.Annotations, t2.Annotations...)
}
if t2.Attest != nil { // merge
t.Attest = append(t.Attest, t2.Attest...)
t.Attest = removeAttestDupes(t.Attest)
Expand Down Expand Up @@ -766,6 +771,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
t.Platforms = o.ArrValue
case "output":
t.Outputs = o.ArrValue
case "annotations":
t.Annotations = append(t.Annotations, o.ArrValue...)
case "attest":
t.Attest = append(t.Attest, o.ArrValue...)
case "no-cache":
Expand Down Expand Up @@ -1164,6 +1171,16 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
return nil, err
}

annotations, err := buildflags.ParseAnnotations(t.Annotations)
if err != nil {
return nil, err
}
for _, e := range bo.Exports {
for k, v := range annotations {
e.Attrs[k.String()] = v
}
}

attests, err := buildflags.ParseAttests(t.Attest)
if err != nil {
return nil, err
Expand Down
28 changes: 28 additions & 0 deletions bake/bake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1460,3 +1460,31 @@ func TestAttestDuplicates(t *testing.T) {
"provenance": ptrstr("type=provenance,mode=max"),
}, opts["default"].Attests)
}

func TestAnnotations(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(
`target "app" {
output = ["type=image,name=foo"]
annotations = ["manifest[linux/amd64]:foo=bar"]
}`),
}
ctx := context.TODO()
m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)

bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)

require.Equal(t, 1, len(g))
require.Equal(t, []string{"app"}, g["default"].Targets)

require.Equal(t, 1, len(m))
require.Contains(t, m, "app")
require.Equal(t, "type=image,name=foo", m["app"].Outputs[0])
require.Equal(t, "manifest[linux/amd64]:foo=bar", m["app"].Annotations[0])

require.Len(t, bo["app"].Exports, 1)
require.Equal(t, "bar", bo["app"].Exports[0].Attrs["annotation-manifest[linux/amd64].foo"])
}
13 changes: 13 additions & 0 deletions commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import (

type buildOptions struct {
allow []string
annotations []string
buildArgs []string
cacheFrom []string
cacheTo []string
Expand Down Expand Up @@ -159,6 +160,16 @@ func (o *buildOptions) toControllerOptions() (*controllerapi.BuildOptions, error
}
}

annotations, err := buildflags.ParseAnnotations(o.annotations)
if err != nil {
return nil, err
}
for _, e := range opts.Exports {
for k, v := range annotations {
e.Attrs[k.String()] = v
}
}

opts.CacheFrom, err = buildflags.ParseCacheEntry(o.cacheFrom)
if err != nil {
return nil, err
Expand Down Expand Up @@ -458,6 +469,8 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {

flags.StringSliceVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure")`)

flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")

flags.StringArrayVar(&options.buildArgs, "build-arg", []string{}, "Set build-time variables")

flags.StringArrayVar(&options.cacheFrom, "cache-from", []string{}, `External cache sources (e.g., "user/app:cache", "type=local,src=path/to/dir")`)
Expand Down
19 changes: 1 addition & 18 deletions commands/imagetools/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@ 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 {
Expand Down Expand Up @@ -160,7 +155,7 @@ func runCreate(dockerCli command.Cli, in createOptions, args []string) error {
}
}

dt, desc, err := r.Combine(ctx, srcs, ann)
dt, desc, err := r.Combine(ctx, srcs, in.annotations)
if err != nil {
return err
}
Expand Down Expand Up @@ -270,18 +265,6 @@ 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

Expand Down
21 changes: 21 additions & 0 deletions docs/bake-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ The following table shows the complete list of attributes that you can assign to
| Name | Type | Description |
| ----------------------------------------------- | ------- | -------------------------------------------------------------------- |
| [`args`](#targetargs) | Map | Build arguments |
| [`annotations`](#targetannotations) | List | Exporter annotations |
| [`attest`](#targetattest) | List | Build attestations |
| [`cache-from`](#targetcache-from) | List | External cache sources |
| [`cache-to`](#targetcache-to) | List | External cache destinations |
Expand Down Expand Up @@ -171,6 +172,26 @@ target "db" {
}
```

### `target.annotations`

The `annotations` attribute is a shortcut to allow you to easily set a list of
annotations on the target.

```hcl
target "default" {
output = ["type=image,name=foo"]
annotations = ["key=value"]
}
```

is the same as

```hcl
target "default" {
output = ["type=image,name=foo,annotation.key=value"]
}
```

### `target.attest`

The `attest` attribute lets you apply [build attestations][attestations] to the target.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/buildx_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Start a build
|:-------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------|:----------|:----------------------------------------------------------------------------------------------------|
| [`--add-host`](https://docs.docker.com/engine/reference/commandline/build/#add-host) | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| [`--allow`](#allow) | `stringSlice` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
| `--annotation` | `stringArray` | | Add annotation to the image |
| [`--attest`](#attest) | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |
| [`--build-context`](#build-context) | `stringArray` | | Additional build contexts (e.g., name=path) |
Expand Down
45 changes: 45 additions & 0 deletions tests/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/moby/buildkit/util/testutil/integration"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand All @@ -40,6 +41,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildMobyFromLocalImage,
testBuildDetailsLink,
testBuildProgress,
testBuildAnnotations,
}

func testBuild(t *testing.T, sb integration.Sandbox) {
Expand Down Expand Up @@ -313,3 +315,46 @@ func testBuildProgress(t *testing.T, sb integration.Sandbox) {
require.Contains(t, string(plainOutput), "[internal] load build definition from Dockerfile")
require.Contains(t, string(plainOutput), "[base 1/3] FROM docker.io/library/busybox:latest")
}

func testBuildAnnotations(t *testing.T, sb integration.Sandbox) {
if sb.Name() == "docker" {
t.Skip("annotations not supported on docker worker")
}

dir := createTestProject(t)

registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
target := registry + "/buildx/registry:latest"

annotations := []string{
"--annotation", "example1=www",
"--annotation", "index:example2=xxx",
"--annotation", "manifest:example3=yyy",
"--annotation", "manifest-descriptor[" + platforms.DefaultString() + "]:example4=zzz",
}
out, err := buildCmd(sb, withArgs(annotations...), withArgs(fmt.Sprintf("--output=type=image,name=%s,push=true", target), dir))
require.NoError(t, err, string(out))

desc, provider, err := contentutil.ProviderFromRef(target)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)

pk := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
img := imgs.Find(pk)
require.NotNil(t, img)

require.NotNil(t, imgs.Index)
assert.Equal(t, "xxx", imgs.Index.Annotations["example2"])

require.NotNil(t, img.Manifest)
assert.Equal(t, "www", img.Manifest.Annotations["example1"])
assert.Equal(t, "yyy", img.Manifest.Annotations["example3"])

require.NotNil(t, img.Desc)
assert.Equal(t, "zzz", img.Desc.Annotations["example4"])
}
46 changes: 46 additions & 0 deletions util/buildflags/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package buildflags

import (
"encoding/csv"
"regexp"
"strings"

"github.com/containerd/containerd/platforms"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -74,3 +78,45 @@ func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
}
return outs, nil
}

func ParseAnnotations(inp []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+)$`)
annotations := make(map[exptypes.AnnotationKey]string)
for _, inp := range inp {
k, v, ok := strings.Cut(inp, "=")
if !ok {
return nil, errors.Errorf("invalid annotation %q, expected key=value", inp)
}

groups := annotationRegexp.FindStringSubmatch(k)
if groups == nil {
return nil, errors.Errorf("invalid annotation format, expected <type>:<key>=<value>, got %q", inp)
}

typ, platform, key := groups[1], groups[2], groups[3]
switch typ {
case "":
case exptypes.AnnotationIndex, exptypes.AnnotationIndexDescriptor, exptypes.AnnotationManifest, exptypes.AnnotationManifestDescriptor:
default:
return nil, errors.Errorf("unknown annotation type %q", typ)
}

var ociPlatform *ocispecs.Platform
if platform != "" {
p, err := platforms.Parse(platform)
if err != nil {
return nil, errors.Wrapf(err, "invalid platform %q", platform)
}
ociPlatform = &p
}

ak := exptypes.AnnotationKey{
Type: typ,
Platform: ociPlatform,
Key: key,
}
annotations[ak] = v
}
return annotations, nil
}
Loading

0 comments on commit 66e6dab

Please sign in to comment.