Skip to content

Commit

Permalink
exporter: detect if multi-platform is set
Browse files Browse the repository at this point in the history
This is required now that the correspondence between platforms and refs
has been broken. Without it, there's no way to detect between the
following cases from the output that they emit:

	buildctl build --opt platform=linux/amd64 --opt attest:sbom
	buildctl build --opt platform=linux/amd64 --opt attest:sbom --opt build-arg:BUILDKIT_MULTI_PLATFORM=true

We'd like the former to produce a flat file structure through the local
exporter, and the latter to produce an explicitly nested file structure
through the same exporter. However, both of these produce Refs instead
of a singular Ref, so we can't just look at the Result to know which
one.

Similar to how we handle the SOURCE_DATE_EPOCH build-arg, we can handle
the multi-platform args at the control API boundary, setting the
explicit option multi-platform in the exporter if it is set. In the
future, we can guide users away from the BUILDKIT_MULTI_PLATFORM args
entirely, and towards the multi-platform exporter option if we want.

Signed-off-by: Justin Chadwell <me@jedevc.com>
  • Loading branch information
jedevc committed Dec 6, 2022
1 parent 5f765da commit 583e480
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 7 deletions.
11 changes: 11 additions & 0 deletions control/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
controlgateway "github.com/moby/buildkit/control/gateway"
"github.com/moby/buildkit/exporter"
"github.com/moby/buildkit/exporter/util/epoch"
"github.com/moby/buildkit/exporter/util/multiplatform"
"github.com/moby/buildkit/frontend"
"github.com/moby/buildkit/frontend/attestations"
"github.com/moby/buildkit/session"
Expand Down Expand Up @@ -304,6 +305,16 @@ func (c *Controller) Solve(ctx context.Context, req *controlapi.SolveRequest) (*
}
}

// if multi-platform is set, enable it for the exporter
if v, ok := multiplatform.ParseFrontend(req.FrontendAttrs); ok {
if _, ok := req.ExporterAttrs[multiplatform.KeyMultiPlatform]; !ok {
if req.ExporterAttrs == nil {
req.ExporterAttrs = make(map[string]string)
}
req.ExporterAttrs[multiplatform.KeyMultiPlatform] = v
}
}

if req.Exporter != "" {
exp, err := w.Exporter(req.Exporter, c.opt.SessionManager)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions exporter/containerimage/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

cacheconfig "github.com/moby/buildkit/cache/config"
"github.com/moby/buildkit/exporter/util/epoch"
"github.com/moby/buildkit/exporter/util/multiplatform"
"github.com/moby/buildkit/util/compression"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -34,6 +35,7 @@ type ImageCommitOpts struct {
BuildInfoAttrs bool
Annotations AnnotationsGroup
Epoch *time.Time
MultiPlatform *bool
}

func (c *ImageCommitOpts) Load(opt map[string]string) (map[string]string, error) {
Expand All @@ -50,6 +52,11 @@ func (c *ImageCommitOpts) Load(opt map[string]string) (map[string]string, error)
return nil, err
}

c.MultiPlatform, opt, err = multiplatform.ParseAttr(opt)
if err != nil {
return nil, err
}

for k, v := range opt {
var err error
switch k {
Expand Down
33 changes: 30 additions & 3 deletions exporter/containerimage/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,20 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session
return nil, errors.Errorf("unable to export multiple refs, missing platforms mapping")
}

multiPlatform := len(inp.Refs) > 0

var p exptypes.Platforms
if ok && len(platformsBytes) > 0 {
if err := json.Unmarshal(platformsBytes, &p); err != nil {
return nil, errors.Wrapf(err, "failed to parse platforms passed to exporter")
}
if len(p.Platforms) > 1 {
multiPlatform = true
}
}

if opts.MultiPlatform != nil {
multiPlatform = *opts.MultiPlatform
}

if opts.Epoch == nil {
Expand All @@ -92,8 +101,26 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session
}
}

if len(inp.Refs) == 0 {
remotes, err := ic.exportLayers(ctx, opts.RefCfg, session.NewGroup(sessionID), inp.Ref)
if !multiPlatform {
if len(p.Platforms) > 1 {
return nil, errors.Errorf("unable to export without multi-platform, multiple platforms passed to exporter")
}

var ref cache.ImmutableRef
if inp.Ref != nil {
ref = inp.Ref
} else if len(p.Platforms) > 0 {
p := p.Platforms[0]
if _, ok := inp.Attestations[p.ID]; ok {
return nil, errors.Errorf("unable to export attestations without multi-platform")
}
ref = inp.Refs[p.ID]
} else if len(inp.Refs) == 1 {
for _, ref = range inp.Refs {
}
}

remotes, err := ic.exportLayers(ctx, opts.RefCfg, session.NewGroup(sessionID), ref)
if err != nil {
return nil, err
}
Expand All @@ -112,7 +139,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session
return nil, errors.Errorf("index annotations not supported for single platform export")
}

mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, opts, inp.Ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], annotations, inp.Metadata[exptypes.ExporterInlineCache], dtbi, opts.Epoch, session.NewGroup(sessionID))
mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, opts, ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], annotations, inp.Metadata[exptypes.ExporterInlineCache], dtbi, opts.Epoch, session.NewGroup(sessionID))
if err != nil {
return nil, err
}
Expand Down
22 changes: 20 additions & 2 deletions exporter/local/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/moby/buildkit/exporter"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/exporter/util/epoch"
"github.com/moby/buildkit/exporter/util/multiplatform"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/solver/result"
Expand Down Expand Up @@ -46,10 +47,16 @@ func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exp
return nil, err
}

multiPlatform, _, err := multiplatform.ParseAttr(opt)
if err != nil {
return nil, err
}

i := &localExporterInstance{
localExporter: e,
opts: CreateFSOpts{
Epoch: tm,
Epoch: tm,
MultiPlatform: multiPlatform,
},
}

Expand Down Expand Up @@ -93,6 +100,8 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
return nil, err
}

isMap := len(inp.Refs) > 0

platformsBytes, ok := inp.Metadata[exptypes.ExporterPlatformsKey]
if len(inp.Refs) > 0 && !ok {
return nil, errors.Errorf("unable to export multiple refs, missing platforms mapping")
Expand All @@ -103,8 +112,17 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
if err := json.Unmarshal(platformsBytes, &p); err != nil {
return nil, errors.Wrapf(err, "failed to parse platforms passed to exporter")
}
if len(p.Platforms) > 1 {
isMap = true
}
}

if e.opts.MultiPlatform != nil {
isMap = *e.opts.MultiPlatform
}
if !isMap && len(p.Platforms) > 1 {
return nil, errors.Errorf("unable to export multiple platforms without map")
}
isMap := len(p.Platforms) > 1

now := time.Now().Truncate(time.Second)

Expand Down
1 change: 1 addition & 0 deletions exporter/local/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
type CreateFSOpts struct {
Epoch *time.Time
AttestationPrefix string
MultiPlatform *bool
}

func CreateFS(ctx context.Context, sessionID string, k string, ref cache.ImmutableRef, refs map[string]cache.ImmutableRef, attestations []result.Attestation, defaultTime time.Time, opt CreateFSOpts) (fsutil.FS, func() error, error) {
Expand Down
22 changes: 20 additions & 2 deletions exporter/tar/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/exporter/local"
"github.com/moby/buildkit/exporter/util/epoch"
"github.com/moby/buildkit/exporter/util/multiplatform"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/solver/result"
Expand Down Expand Up @@ -48,12 +49,18 @@ func New(opt Opt) (exporter.Exporter, error) {
func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) {
li := &localExporterInstance{localExporter: e}

tm, _, err := epoch.ParseAttr(opt)
tm, opt, err := epoch.ParseAttr(opt)
if err != nil {
return nil, err
}
li.opts.Epoch = tm

multiPlatform, opt, err := multiplatform.ParseAttr(opt)
if err != nil {
return nil, err
}
li.opts.MultiPlatform = multiPlatform

for k, v := range opt {
switch k {
case preferNondistLayersKey:
Expand Down Expand Up @@ -126,6 +133,8 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
}, nil
}

isMap := len(inp.Refs) > 0

platformsBytes, ok := inp.Metadata[exptypes.ExporterPlatformsKey]
if len(inp.Refs) > 0 && !ok {
return nil, errors.Errorf("unable to export multiple refs, missing platforms mapping")
Expand All @@ -136,8 +145,17 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
if err := json.Unmarshal(platformsBytes, &p); err != nil {
return nil, errors.Wrapf(err, "failed to parse platforms passed to exporter")
}
if len(p.Platforms) > 1 {
isMap = true
}
}

if e.opts.MultiPlatform != nil {
isMap = *e.opts.MultiPlatform
}
if !isMap && len(p.Platforms) > 1 {
return nil, errors.Errorf("unable to export multiple platforms without map")
}
isMap := len(p.Platforms) > 1

var fs fsutil.FS

Expand Down
45 changes: 45 additions & 0 deletions exporter/util/multiplatform/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package multiplatform

import (
"strconv"

"github.com/pkg/errors"
)

const (
frontendMultiPlatform = "multi-platform"
frontendMultiPlatformArg = "build-arg:BUILDKIT_MULTI_PLATFORM"

KeyMultiPlatform = "multi-platform"
)

func ParseFrontend(opt map[string]string) (string, bool) {
if v, ok := opt[frontendMultiPlatform]; ok {
return v, true
}
if v, ok := opt[frontendMultiPlatformArg]; ok {
return v, true
}
return "", false
}

func ParseAttr(opt map[string]string) (*bool, map[string]string, error) {
rest := make(map[string]string, len(opt))

var multiPlatform *bool

for k, v := range opt {
switch k {
case KeyMultiPlatform:
b, err := strconv.ParseBool(v)
if err != nil {
return nil, nil, errors.Errorf("invalid boolean value %s", v)
}
multiPlatform = &b
default:
rest[k] = v
}
}

return multiPlatform, rest, nil
}

0 comments on commit 583e480

Please sign in to comment.