mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-19 01:47:43 +08:00
build: add --annotation shortcut flag
This extracts the same logic for parsing annotations from the imagetools create command, and allows the same flags to be attached to the build command. These annotations are then merged into all provided exporters. Signed-off-by: Justin Chadwell <me@jedevc.com>
This commit is contained in:
parent
8fe2070d10
commit
a59058e8a5
@ -54,6 +54,7 @@ import (
|
|||||||
|
|
||||||
type buildOptions struct {
|
type buildOptions struct {
|
||||||
allow []string
|
allow []string
|
||||||
|
annotations []string
|
||||||
buildArgs []string
|
buildArgs []string
|
||||||
cacheFrom []string
|
cacheFrom []string
|
||||||
cacheTo []string
|
cacheTo []string
|
||||||
@ -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)
|
opts.CacheFrom, err = buildflags.ParseCacheEntry(o.cacheFrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -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.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.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")`)
|
flags.StringArrayVar(&options.cacheFrom, "cache-from", []string{}, `External cache sources (e.g., "user/app:cache", "type=local,src=path/to/dir")`)
|
||||||
|
@ -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")
|
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
|
var defaultRepo *string
|
||||||
if len(repos) == 1 {
|
if len(repos) == 1 {
|
||||||
for repo := range repos {
|
for repo := range repos {
|
||||||
@ -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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -270,18 +265,6 @@ func parseSource(in string) (*imagetools.Source, error) {
|
|||||||
return &s, nil
|
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 {
|
func createCmd(dockerCli command.Cli, opts RootOptions) *cobra.Command {
|
||||||
var options createOptions
|
var options createOptions
|
||||||
|
|
||||||
|
@ -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`) |
|
| [`--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`) |
|
| [`--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`) |
|
| [`--attest`](#attest) | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
|
||||||
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |
|
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |
|
||||||
| [`--build-context`](#build-context) | `stringArray` | | Additional build contexts (e.g., name=path) |
|
| [`--build-context`](#build-context) | `stringArray` | | Additional build contexts (e.g., name=path) |
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/moby/buildkit/util/testutil/integration"
|
"github.com/moby/buildkit/util/testutil/integration"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ var buildTests = []func(t *testing.T, sb integration.Sandbox){
|
|||||||
testBuildMobyFromLocalImage,
|
testBuildMobyFromLocalImage,
|
||||||
testBuildDetailsLink,
|
testBuildDetailsLink,
|
||||||
testBuildProgress,
|
testBuildProgress,
|
||||||
|
testBuildAnnotations,
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBuild(t *testing.T, sb integration.Sandbox) {
|
func testBuild(t *testing.T, sb integration.Sandbox) {
|
||||||
@ -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), "[internal] load build definition from Dockerfile")
|
||||||
require.Contains(t, string(plainOutput), "[base 1/3] FROM docker.io/library/busybox:latest")
|
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"])
|
||||||
|
}
|
||||||
|
@ -2,10 +2,14 @@ package buildflags
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/platforms"
|
||||||
controllerapi "github.com/docker/buildx/controller/pb"
|
controllerapi "github.com/docker/buildx/controller/pb"
|
||||||
"github.com/moby/buildkit/client"
|
"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"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,3 +78,45 @@ func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
|
|||||||
}
|
}
|
||||||
return outs, nil
|
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
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
@ -14,6 +13,7 @@ import (
|
|||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
"github.com/containerd/containerd/remotes"
|
"github.com/containerd/containerd/remotes"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/buildx/util/buildflags"
|
||||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||||
"github.com/moby/buildkit/util/contentutil"
|
"github.com/moby/buildkit/util/contentutil"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
@ -28,7 +28,7 @@ type Source struct {
|
|||||||
Ref reference.Named
|
Ref reference.Named
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[string]string) ([]byte, ocispec.Descriptor, error) {
|
func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string) ([]byte, ocispec.Descriptor, error) {
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
dts := make([][]byte, len(srcs))
|
dts := make([][]byte, len(srcs))
|
||||||
@ -143,7 +143,7 @@ func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[string]s
|
|||||||
// annotations are only allowed on OCI indexes
|
// annotations are only allowed on OCI indexes
|
||||||
indexAnnotation := make(map[string]string)
|
indexAnnotation := make(map[string]string)
|
||||||
if mt == ocispec.MediaTypeImageIndex {
|
if mt == ocispec.MediaTypeImageIndex {
|
||||||
annotations, err := parseAnnotations(ann)
|
annotations, err := buildflags.ParseAnnotations(ann)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ocispec.Descriptor{}, err
|
return nil, ocispec.Descriptor{}, err
|
||||||
}
|
}
|
||||||
@ -297,40 +297,3 @@ func detectMediaType(dt []byte) (string, error) {
|
|||||||
|
|
||||||
return images.MediaTypeDockerSchema2ManifestList, nil
|
return images.MediaTypeDockerSchema2ManifestList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAnnotations(ann map[string]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 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]
|
|
||||||
switch typ {
|
|
||||||
case "":
|
|
||||||
case exptypes.AnnotationIndex, exptypes.AnnotationIndexDescriptor, exptypes.AnnotationManifest, exptypes.AnnotationManifestDescriptor:
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("unknown annotation type %q", typ)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
ak := exptypes.AnnotationKey{
|
|
||||||
Type: typ,
|
|
||||||
Platform: ociPlatform,
|
|
||||||
Key: key,
|
|
||||||
}
|
|
||||||
annotations[ak] = v
|
|
||||||
}
|
|
||||||
return annotations, nil
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user